The download extension saves a response as a file download instead of swapping it into the DOM. It streams the response body and fires progress events.
It activates in three ways:
hx-swap="download"on the element- The server responds with
Content-Disposition: attachment(auto-detected, no attribute needed) - The server responds with
HX-Download: <url>(extension fetches that URL as the download, while the original response is still swapped normally)
Installing
<script src="https://cdn.jsdelivr.net/npm/htmx.org@next/dist/htmx.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/htmx.org@next/dist/ext/hx-download.js"></script>
Usage
Explicit swap style
Use hx-swap="download" to always treat the response as a download:
<button hx-get="/files/report.pdf" hx-swap="download"> Download Report </button>
Auto-detect via Content-Disposition
If the server returns Content-Disposition: attachment, the extension triggers a download automatically — no hx-swap needed:
<button hx-get="/files/report.pdf" hx-target="#result"> Download Report </button>
Content-Disposition: attachment; filename="report.pdf"
HX-Download header
When the backend cannot stream the file directly as the htmx response (e.g. it needs to redirect to a separate download endpoint), return an HX-Download header pointing to the file URL. The extension will fetch that URL as the download while htmx processes the original response body as a normal swap:
<button hx-get="/prepare-download" hx-target="#status"> Download Report </button>
HX-Download: /files/report.pdf Content-Type: text/html <span>Your download has started...</span>
The <span> is swapped into #status and the file at /files/report.pdf is downloaded simultaneously.
This approach also avoids the indicator/disabled-element cleanup problem that occurs with HX-Redirect pointing to a download URL — indicators are cleared correctly after the original request completes.
Progress Bar
The extension fires events on the source element as the response streams in:
<button hx-get="/files/report.pdf" hx-swap="download"> Download Report </button> <progress id="prog" value="0" max="100"></progress> <script> document.body.addEventListener("htmx:download:progress", e => { document.getElementById("prog").value = e.detail.percent }) </script>
The server must send a Content-Length header for percentage-based progress. Without it, percent will be null and only loaded (bytes received) is available.
Events
| Event | Detail | Description |
|---|---|---|
htmx:download:start | {total} | Response headers received, streaming begins. total is null if no Content-Length |
htmx:download:progress | {loaded, total, percent} | Fired for each chunk received. percent is null if no Content-Length |
htmx:download:complete | {filename, size} | File download triggered in the browser |
Notes
- Binary files (PDFs, images, archives) are handled correctly.
- The filename is extracted from the
Content-Dispositionheader (filenameorfilename*=UTF-8''...), falling back to the URL’s last path segment. - Cancellation works automatically via htmx’s built-in request abort handling.
- For
HX-Download, indicators and disabled elements are cleared correctly after the original request completes — the secondary fetch runs independently.