History, errors, and enhancement

Manage focus after a swap

Keep keyboard users oriented when the element they were using is replaced or an error appears elsewhere.

Swapping HTML can disorient keyboard users. Focus might land on nothing, or stay on an element that no longer exists. HTMX preserves focus for compatible inputs that keep the same stable id across a swap.

This makes replacing a validation form less disruptive:

<label for="task-title">Task</label>
<input id="task-title" name="title" value="">

Keep that ID in both the initial and error form. Submit an empty title, get a 422, and the cursor should stay in #task-title after HTMX swaps the form.

A common failure: you use outerHTML on the form but change the input ID in the error template. HTMX cannot restore focus because the old node is gone and the new input has a different ID. Fix: keep id="task-title" identical in every version of the form the server returns.

Use focus-scroll:true on hx-swap only when restoring focus should also bring the field back into view. The default avoids unexpected scrolling during a long request.

Some changes require an intentional move. If validation reveals an error summary, make it programmatically focusable and focus it after the swap:

<div id="form-errors" tabindex="-1" role="alert">
  Fix the highlighted fields.
</div>

A small htmx:afterSwap listener can focus that element when it appears. Screen readers then announce the summary immediately.

When deleting a task row, move focus to the next <li> or to the list heading. Losing focus to <body> is disorienting. After a successful hx-swap="delete", the focused button no longer exists. A three-line afterSwap handler that checks event.detail.target is enough.

Do not move focus for every count, status, or background update. Keyboard users need continuity, not surprise.

Test create, validation failure, deletion, and history restoration using only the keyboard. Tab order and focus targets should still make sense after each swap.

After completing a task row, focus often belongs on the next action in that row or on the list heading. Watch the focus ring in DevTools while you Tab through. If it disappears, you skipped an explicit focus move after a swap that removed the active element.

Lesson completed