Triggers, forms, and feedback

Enhance a real form

Keep labels, action, method, validation, submit behavior, and server response useful before adding asynchronous replacement.

Progressive enhancement starts with a form that works without JavaScript. Add HTMX on top.

Build the complete task form first:

<form id="new-task" action="/tasks" method="post"
  hx-post="/tasks"
  hx-target="#task-list"
  hx-swap="outerHTML">
  <label>Task <input name="title" required></label>
  <button type="submit">Add</button>
</form>

The action and method define normal browser submission. Without JavaScript, the browser posts to /tasks and navigates. The server should return a full page with either the updated list or a form containing validation errors.

With HTMX, a successful create returns a complete replacement for #task-list. In DevTools you should see POST /tasks with status 200 and a response body that is a <ul id="task-list"> containing every task row.

Validation is trickier because the error form belongs in a different target. Submit an empty title and the server should respond with:

HTTP/1.1 422 Unprocessable Content
HX-Retarget: #new-task
HX-Reswap: outerHTML

The response body is the whole form again, with the title field preserved and a message such as “Title is required” next to the input. Configure htmx:beforeSwap once to allow the intentional 422 fragment. Without that listener, HTMX treats the status as an error and leaves the old form in place while the user sees nothing.

The browser’s required check improves feedback but is not server validation. Disable JavaScript, submit an empty title, and confirm the server returns the same rule in a full page. If only the HTMX path validates, you do not have progressive enhancement.

Field-level errors work best when each message sits next to its input and the server re-renders the same field names. That way HTMX can swap the whole form and nothing breaks for assistive tech.

Finally, disable JavaScript entirely. Creating a valid task and correcting an invalid one should both work through full-page responses. The HTMX path should be a faster version of the same contract, not a separate app.

Lesson completed