htmx 4.0 is under construction — migration guide

File Upload

Upload files with progress and validation

Basic usage

On the client, set hx-encoding to multipart/form-data:

<form hx-post="/upload" hx-encoding="multipart/form-data"> <input type="file" name="file"> <input type="text" name="name"> <button>Submit</button> </form>

On the server, respond with a success or error message:

<p>File uploaded successfully.</p>

Preserving file selection on errors

When a form re-renders with validation errors, file inputs lose their selection. Add hx-preserve to keep it:

<form hx-post="..." hx-swap="outerHTML" hx-encoding="multipart/form-data"> <input hx-preserve type="file" name="file"> <input type="text" name="name"> <button>Submit</button> </form>

Try it in the demo: select a file, then submit with empty fields. The form re-renders with errors, but your file selection stays.

Alternatively, place the file input outside the swap target using the form attribute:

<input form="my-form" type="file" name="file"> <form id="my-form" hx-post="..." hx-encoding="multipart/form-data"> <button>Submit</button> </form>

The input is outside the form element, so it is never replaced during swaps.

Upload progress

htmx 4.x uses the native fetch API. fetch supports upload progress monitoring in some browsers, but cross-browser support is limited. For reliable progress tracking, use XMLHttpRequest directly:

<form id="upload-form" enctype="multipart/form-data"> <input type="file" name="file"> <button>Upload</button> <progress id="progress" value="0" max="100"></progress> </form> <script> document.querySelector('#upload-form').addEventListener('submit', (e) => { e.preventDefault(); const xhr = new XMLHttpRequest(); xhr.upload.addEventListener('progress', (evt) => { document.querySelector('#progress').value = (evt.loaded / evt.total) * 100; }); xhr.open('POST', '/upload'); xhr.send(new FormData(e.target)); }); </script>