</> htmx
🚧 htmx 4.0 is under construction. Read changes →

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).

Try submitting without selecting a file

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.