hx-download
Download files with hx-swap='download'
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@4.0.0/dist/htmx.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/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.
Licensed under the Zero-Clause BSD License.
https://four.htmx.org/extensions/hx-download/