Session expiry
The session that expired over lunch should not eat the click that
discovered it. In this recipe any protected action’s 401 carries a
login <dialog> retargeted at a shared host —
installRemoteDialog
(shipped) opens it — and the auto-installed installSessionExpiry()
remembers the interrupted request and replays it once the login
response fires hc:sessionrenewed. The user’s action completes instead
of vanishing. Covered by the
versioning policy.
Also known as: session timeout, re-login flow.
Live demo
Section titled “Live demo”The demo session starts expired: click Approve, sign in (any
password; wrong fails inline), and watch the approval complete by
itself. Expire session re-arms the flow.
The session starts expired — click Approve.
The markup
Section titled “The markup”<button class="hc-button" data-variant="primary" type="button" data-hx-post="/tickets/7/approve" data-hx-target="#status" data-hx-swap="innerHTML"> Approve</button><p class="hc-field__hint" id="status" aria-live="polite"></p>
<!-- One shared host at body end --><div id="error-dialog" data-hc-remote-dialog-root data-hc-session-expiry></div>The host wears two hats: data-hc-remote-dialog-root opens whatever
dialog the server swaps in; data-hc-session-expiry arms the replay
bridge.
Server response contract
Section titled “Server response contract”| Case | Response |
|---|---|
| protected endpoint, session expired, htmx request | 401 + HX-Retarget: #error-dialog + HX-Reswap: innerHTML + the login dialog fragment. The server must 401 before acting — replay safety depends on it |
| login success | 200 + empty body + HX-Trigger: {"hc:sessionrenewed": {}} — the bridge closes the dialog and replays |
| login failure | 422 + HX-Retarget: #error-dialog + HX-Reswap: innerHTML + the dialog re-rendered with field-errors inline — the login form posts with data-hx-target="this" / data-hx-swap="none", so without the headers the body would be swallowed |
| non-htmx request, session expired | 303 to the login page with ?next= |
Replay semantics
Section titled “Replay semantics”One slot, latest wins: parallel interruptions replay only the last.
The replay goes through htmx.ajax(verb, path, { source, values }),
re-running the full pipeline — a rotated CSRF token is picked up fresh
by installCsrfHeader, indicators and targets behave as if the user
clicked again. Multi-value fields collapse to their last value; if the
interrupted element left the DOM, the replay is skipped. Nothing
survives a full page load.
Accessibility
Section titled “Accessibility”- The dialog is a real
<dialog>viashowModal()— native focus trap andEscape;aria-labelledbynames it. - Cancel is a
<form method="dialog">button — declarative, CSP-safe. - The replayed outcome lands in the action’s own
aria-livetarget, so completion is announced.
Related
Section titled “Related”- edit-conflict — the 409 sibling on the same error-dialog host.
- Errors & recovery — the full status-code map and the consolidated allowance.
- remote-dialog — the host mechanism this recipe rides.
- field-errors — the 422 allowance shape this recipe extends to 401.