htmx 4.0 is under construction — migration guide

hx-download

Save responses as file downloads with progress events

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

EventDetailDescription
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-Disposition header (filename or filename*=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.