Htmx Forms
Form handling, validation, and submission patterns with HTMX
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 linesForm 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-Redirectheader) - 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 redirectHX-Retarget: #selector— overrides the originalhx-targetHX-Reswap: outerHTML— overrides the originalhx-swapHX-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-selectto extract specific fragments, or swap only the error message areas. -
Overusing inline validation on every keystroke. Triggering server validation on
keyupfor every field creates excessive server load and a noisy user experience. Useblur changedas 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-eltto prevent double submission. This is simpler and more reliable than JavaScript-based button disabling. - Return
HX-Redirectfor successful creates. After a POST that creates a resource, send back anHX-Redirectheader 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-targetpoints to the form or a parent, the re-render can displace the user's cursor. Target the smallest necessary container or usehx-selectto 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:responseErroror configurehtmx.config.responseHandlingto handle error codes gracefully. - Overusing inline validation. Validating every keystroke creates excessive server load and a noisy UX. Use
blur changedas 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
Related Skills
Htmx Active Search
Active search, typeahead, and autocomplete patterns using HTMX triggers and debouncing
Htmx Alpine Integration
Combining Alpine.js with HTMX for client-side state and interactivity alongside hypermedia-driven updates
Htmx Backend Patterns
Server-side patterns for HTMX including partial templates, response headers, and middleware
Htmx Basics
Core HTMX attributes and fundamental patterns for hypermedia-driven web development
Htmx Infinite Scroll
Infinite scroll and lazy loading patterns using HTMX revealed and intersect triggers
Htmx Progressive Enhancement
Progressive enhancement strategies for building HTMX applications that work without JavaScript