File Uploads
Upload files with progress tracking and validation handling. Use multipart/form-data encoding.
Upload with Progress
Create a form with multipart encoding:
<form hx-encoding="multipart/form-data" hx-post="/upload">
<input type="file" name="file">
<button>Upload</button>
<progress id="progress" value="0" max="100"></progress>
</form>
Listen for progress events and update the progress bar:
<script>
htmx.on('#form', 'htmx:xhr:progress', function(evt) {
htmx.find('#progress').setAttribute('value', evt.detail.loaded / evt.detail.total * 100);
});
</script>
With Hyperscript
Use hyperscript for cleaner syntax:
<form hx-encoding="multipart/form-data" hx-post="/upload"
_="on htmx:xhr:progress(loaded, total)
set #progress.value to (loaded/total)*100">
<input type="file" name="file">
<button>Upload</button>
<progress id="progress" value="0" max="100"></progress>
</form>
Hyperscript lets you destructure event details directly into variables.
Keep File Selection on Errors
When forms return with validation errors, file inputs lose their selection. Preserve them with hx-preserve or by moving the input outside the swap target.
Using hx-preserve
Add hx-preserve to keep the file selection:
<form enctype="multipart/form-data" hx-post="/submit">
<input hx-preserve type="file" name="file">
<button>Submit</button>
</form>
The file stays selected when the form swaps with error messages.
Important: Only add hx-preserve to the input itself, not error containers. The server can conditionally remove hx-preserve when the file has errors (like invalid type).
Move Input Outside Form
Place the file input outside the swap target:
<input form="form-id" type="file" name="file">
<form id="form-id" enctype="multipart/form-data" hx-post="/submit">
<button>Submit</button>
</form>
The form attribute links the input to the form. Since the input is outside the swap target, it never gets replaced.