File upload
file-upload is the blessed multipart pattern: selecting a file and
uploading shows a live progress bar, appends the new file’s
fragment to the list, resets the form, and toasts. htmx owns the
transport (data-hx-encoding + htmx:xhr:progress are native); the
only glue is installUploadProgress() driving the native
<progress>. Stable
under the
markup versioning policy.
It needs installUploadProgress(), installToast() and
installFieldErrors() (all in the auto-init
./behaviors
bundle).
Live demo
Section titled “Live demo”Pick a small PDF or PNG and upload it: the progress bar is driven by
htmx:xhr:progress (the installUploadProgress() bridge), and the
response prepends the new item to the list while swapping in a pristine
form out-of-band — the blessed file-input reset. Try a disallowed
extension (say a .txt) to see the retargeted 422 field-error path.
The second form is the dropzone variant — drop a
file onto it (or click) and the same contract plays out against its own
error container and out-of-band reset.
Uploads aren’t stored: the demo derives the item from the request and
forgets it, so reloading resets the list.
Dropzone variant — same endpoint, same contract
The form
Section titled “The form”<form id="upload-form" method="post" action="/files" enctype="multipart/form-data" data-hx-post="/files" data-hx-encoding="multipart/form-data" data-hx-target="#files" data-hx-swap="afterbegin" data-hx-indicator="find progress" data-hx-disabled-elt="find button[type=submit]"> <div id="upload-errors"></div> <div class="hc-field"> <label class="hc-field__label" for="doc">Document</label> <input class="hc-input" id="doc" name="doc" type="file" required accept=".pdf,.png"> </div> <progress class="hc-progress htmx-indicator" data-hc-upload-progress value="0" max="100" aria-label="Upload progress"></progress> <button class="hc-button" data-variant="primary" type="submit">Upload</button></form>
<ul id="files">…server-rendered current files…</ul>The progress bar
Section titled “The progress bar”Two mechanisms, cleanly split:
- Visibility is htmx-native:
data-hx-indicator="find progress"+ thehtmx-indicatorclass show the bar only while the request is in flight. - Value is the bridge:
installUploadProgress()mapshtmx:xhr:progressonto 0–100 — monotonically within a request, because htmx fires that event for both the upload and the response-download phase, and the download’s smalltotalwould otherwise rewind the bar just before it finishes.
Server responses
Section titled “Server responses”| Case | Response |
|---|---|
| Success | 200 — the new item fragment (lands afterbegin in #files) plus the pristine form as an hx-swap-oob fragment (file inputs can’t be reset from markup — the server re-sends the form; htmx re-initializes it) + an HX-Trigger toast |
| Validation failure | 422 + HX-Retarget: #upload-errors + HX-Reswap: innerHTML with the field-errors fragment — the primary path stays attribute-declared; only the exception steers via headers |
Proxy 413 | May arrive before the app sees the request; htmx swaps nothing. The contract has an optional htmx:responseError toast snippet — and keep your app limit below the proxy’s so the friendly 422 wins |
| No JS | 303 post/redirect/get (HX-Request branching, as in mutating-form) |
The server is the validator — client accept/size hints are UX only.
Escape non-ASCII in HX-Trigger headers as \uXXXX
(header values are latin-1 — see the
undo-delete contract).
Degradation and follow-ups
Section titled “Degradation and follow-ups”Without JavaScript the same form posts a correct multipart request natively; progress and inline errors are enhancements.
Dropzone variant
Section titled “Dropzone variant”Swap the plain field for an
hc-dropzone — nothing
else changes. Drops assign the same native input and fire a normal
change, so serialization, progress, the OOB reset and the 422 path
are identical; the fresh form the server re-sends simply contains the
pristine dropzone markup.
The claims here — live progress reaching 100, the out-of-band form
reset, the retargeted 422 — are pinned by a real-multipart browser
test (test-browser/file-upload.spec.mjs).
Related
Section titled “Related”- Progress — the native element the bridge drives.
- Field errors recipe — the
422fragment. - Mutating form recipe — the
HX-Requestbranching andbeforeSwapallowance. - Toast recipe — success feedback.