Skip to main content
Technology & EngineeringHtmx200 lines

Htmx Forms

Form handling, validation, and submission patterns with HTMX

Quick Summary24 lines
You are an expert in building robust forms with HTMX, including server-side validation, inline error display, and multi-step form flows.

## Key Points

- A success fragment (e.g., the saved entity rendered as HTML, or a redirect via `HX-Redirect` header)
- The form re-rendered with validation errors inline
- `HX-Redirect: /path` — tells HTMX to perform a client-side redirect
- `HX-Retarget: #selector` — overrides the original `hx-target`
- `HX-Reswap: outerHTML` — overrides the original `hx-swap`
- `HX-Trigger: eventName` — fires a client-side event after the swap
- **Use 422 (Unprocessable Entity) for validation errors.** HTMX processes non-2xx responses normally by default. A 422 signals a validation failure while still allowing the response body to render.
- **Preserve user input on errors.** When re-rendering the form, populate fields with the submitted values so users do not lose their work.
- **Use `hx-disabled-elt` to prevent double submission.** This is simpler and more reliable than JavaScript-based button disabling.
- **Scope validation endpoints narrowly.** Inline field validation endpoints should validate a single field and return only the error markup for that field.
- **Forgetting `hx-encoding="multipart/form-data"` for file uploads.** Without this, files will not be transmitted. HTMX defaults to URL-encoded form data.
- **Overusing inline validation.** Validating every keystroke creates excessive server load and a noisy UX. Use `blur changed` as the trigger to validate only when the user leaves a modified field.

## Quick Example

```html
<span class="error">This email is already registered.</span>
```
skilldb get htmx-skills/Htmx FormsFull skill: 200 lines
Paste into your CLAUDE.md or agent config

Form Handling and Validation — HTMX

You are an expert in building robust forms with HTMX, including server-side validation, inline error display, and multi-step form flows.

Overview

HTMX transforms traditional form handling by enabling partial page updates on submission, inline field validation, and dynamic form behavior — all while keeping the server as the source of truth. Forms submit via AJAX and the server returns HTML fragments that replace or augment the current page content.

Core Concepts

HTMX Form Submission

Any <form> with an hx-post (or hx-put, hx-patch) attribute submits via AJAX instead of a full page reload. The form values are serialized and sent exactly as a normal form submission would.

Server-Side Validation Model

The server validates input and returns either:

  • A success fragment (e.g., the saved entity rendered as HTML, or a redirect via HX-Redirect header)
  • The form re-rendered with validation errors inline

This keeps all validation logic on the server, eliminating client/server validation drift.

Response Headers for Flow Control

  • HX-Redirect: /path — tells HTMX to perform a client-side redirect
  • HX-Retarget: #selector — overrides the original hx-target
  • HX-Reswap: outerHTML — overrides the original hx-swap
  • HX-Trigger: eventName — fires a client-side event after the swap

Implementation Patterns

Basic Form with Validation Errors

<form hx-post="/contacts" hx-target="#form-container" hx-swap="innerHTML">
  <div id="form-container">
    <div class="field">
      <label for="email">Email</label>
      <input type="email" name="email" id="email" value="">
    </div>
    <div class="field">
      <label for="name">Name</label>
      <input type="text" name="name" id="name" value="">
    </div>
    <button type="submit">Save</button>
  </div>
</form>

Server response on validation failure (returns the form with errors):

<div class="field">
  <label for="email">Email</label>
  <input type="email" name="email" id="email" value="bad-email"
         class="is-invalid" aria-invalid="true">
  <span class="error">Please enter a valid email address.</span>
</div>
<div class="field">
  <label for="name">Name</label>
  <input type="text" name="name" id="name" value="Jane">
</div>
<button type="submit">Save</button>

Inline Field Validation (Validate on Blur)

<form hx-post="/contacts" hx-target="this" hx-swap="outerHTML">
  <div class="field">
    <label for="email">Email</label>
    <input type="email" name="email" id="email"
           hx-post="/contacts/validate-email"
           hx-trigger="blur changed"
           hx-target="next .error-slot"
           hx-swap="innerHTML">
    <div class="error-slot"></div>
  </div>
  <button type="submit">Save</button>
</form>

The /contacts/validate-email endpoint returns either an empty body (valid) or an error message fragment:

<span class="error">This email is already registered.</span>

Multi-Step Wizard Form

<!-- Step 1 -->
<div id="wizard">
  <form hx-post="/wizard/step-2" hx-target="#wizard" hx-swap="innerHTML">
    <h2>Step 1: Personal Info</h2>
    <input name="name" placeholder="Name" required>
    <input name="email" type="email" placeholder="Email" required>
    <button type="submit">Next</button>
  </form>
</div>

Server returns the next step, carrying forward previous values as hidden fields:

<form hx-post="/wizard/step-3" hx-target="#wizard" hx-swap="innerHTML">
  <h2>Step 2: Address</h2>
  <input type="hidden" name="name" value="Jane">
  <input type="hidden" name="email" value="jane@example.com">
  <input name="street" placeholder="Street">
  <input name="city" placeholder="City">
  <button type="submit">Next</button>
  <button hx-get="/wizard/step-1" hx-target="#wizard" hx-swap="innerHTML"
          type="button">Back</button>
</form>

File Upload with Progress

<form hx-post="/upload"
      hx-encoding="multipart/form-data"
      hx-target="#upload-result"
      hx-indicator="#progress">
  <input type="file" name="document">
  <button type="submit">Upload</button>
  <progress id="progress" class="htmx-indicator" value="0" max="100"></progress>
</form>
<div id="upload-result"></div>

Use the htmx:xhr:progress event for real progress tracking:

<script>
  htmx.on("htmx:xhr:progress", function(evt) {
    if (evt.detail.lengthComputable) {
      htmx.find("#progress").setAttribute(
        "value", evt.detail.loaded / evt.detail.total * 100
      );
    }
  });
</script>

Disabling Submit During Request

<form hx-post="/contacts" hx-target="#result">
  <input name="name">
  <button type="submit" hx-disabled-elt="this">
    Save
  </button>
</form>

hx-disabled-elt adds the disabled attribute to the specified element(s) for the duration of the request, preventing double submissions.

Core Philosophy

HTMX form handling embraces the principle that the server is the authoritative validator and the HTML response is the feedback mechanism. When a form is submitted, the server validates the input, and either returns a success response (a redirect, a new row, a confirmation) or returns the form re-rendered with error messages inline. This keeps all validation logic in one place, eliminates the problem of client/server validation drift, and ensures the user always sees the most accurate feedback.

The progressive enhancement model is central to HTMX forms. A form built with standard action and method attributes works without JavaScript. HTMX attributes layer on top to convert the submission to AJAX, swap the response into a targeted area, and provide loading states. If HTMX fails to load, the form still works. This is not just a nice-to-have but a fundamental architectural commitment: the form is always a valid HTML form first.

Server-driven form flows also simplify multi-step wizards, conditional fields, and dynamic validation. Instead of building complex client-side state machines, each form step is a server round trip that returns the next piece of UI. Hidden inputs carry forward previously collected data, and the server decides what to render next based on the current submission. The result is a wizard that is trivially debuggable because every step is a visible HTTP request with a testable response.

Anti-Patterns

  • Validating only on the client. HTML5 validation attributes (required, pattern, type="email") improve UX but are trivially bypassed. The server must always validate, and the HTML response must always reflect the server's verdict.

  • Replacing the entire form on every validation error. Swapping the whole form displaces the user's cursor and focus. Target the smallest necessary container, use hx-select to extract specific fragments, or swap only the error message areas.

  • Overusing inline validation on every keystroke. Triggering server validation on keyup for every field creates excessive server load and a noisy user experience. Use blur changed as the trigger so validation fires only when the user leaves a modified field.

  • Losing user input on validation errors. When re-rendering the form with errors, failing to populate fields with the submitted values forces users to re-enter everything. Always echo back the submitted data in input values.

  • Using JavaScript-only form submission patterns. Building forms that only work through fetch() calls or framework-specific form handlers breaks progressive enhancement and adds unnecessary client-side complexity. Let HTMX handle the submission through its attributes.

Best Practices

  • Always validate on the server. Client-side validation (HTML5 required, pattern) improves UX, but the server must be the authority. Return the form with errors on validation failure using a 422 status code.
  • Use 422 (Unprocessable Entity) for validation errors. HTMX processes non-2xx responses normally by default. A 422 signals a validation failure while still allowing the response body to render.
  • Preserve user input on errors. When re-rendering the form, populate fields with the submitted values so users do not lose their work.
  • Use hx-disabled-elt to prevent double submission. This is simpler and more reliable than JavaScript-based button disabling.
  • Return HX-Redirect for successful creates. After a POST that creates a resource, send back an HX-Redirect header to navigate the user to the new resource, matching PRG (Post/Redirect/Get) semantics.
  • Scope validation endpoints narrowly. Inline field validation endpoints should validate a single field and return only the error markup for that field.

Common Pitfalls

  • Forgetting hx-encoding="multipart/form-data" for file uploads. Without this, files will not be transmitted. HTMX defaults to URL-encoded form data.
  • Replacing the form itself without preserving focus. If hx-target points to the form or a parent, the re-render can displace the user's cursor. Target the smallest necessary container or use hx-select to pick specific fragments.
  • Not handling 4xx/5xx responses. By default HTMX swaps any response into the target. If your server returns an error page for a 500, it will appear inline. Use htmx:responseError or configure htmx.config.responseHandling to handle error codes gracefully.
  • Overusing inline validation. Validating every keystroke creates excessive server load and a noisy UX. Use blur changed as the trigger to validate only when the user leaves a modified field.
  • Losing hidden state across steps in wizard forms. Each step must carry forward all previously collected data as hidden inputs or use server-side session storage.

Install this skill directly: skilldb add htmx-skills

Get CLI access →