Unsaved changes
Losing a half-filled form is the top complaint class in business apps.
data-hc-dirty-guard (the auto-installed installDirtyGuard()) makes
a form honest about its edit state: data-dirty appears on the first
real change, the tab refuses to close silently, boosted navigation
asks first, and a successful save resets the baseline. Client-only —
the save keeps whatever contract the form already follows. Covered by
the versioning policy.
Also known as: dirty check, leave confirmation, beforeunload guard.
Live demo
Section titled “Live demo”The preview is live: edit the title and the badge appears (a plain
form[data-dirty] CSS hook); revert the exact text and it disappears.
Closing the tab while dirty would prompt — the demo form has no server,
so Save here is inert.
<form data-hc-dirty-guard data-hx-post="/reports/42" data-hx-target="#status" data-hx-swap="innerHTML"> <div class="hc-field"> <label class="hc-field__label" for="title">Title</label> <input class="hc-input" id="title" name="title" value="Quarterly report"> </div> <p class="hc-field__hint" id="status" aria-live="polite"></p> <button class="hc-button" data-variant="primary" type="submit">Save</button></form>What the guard does
Section titled “What the guard does”| Moment | Effect |
|---|---|
| first focus in the form | baseline snapshot (via new FormData(form) — installFormat’s canonical values, so display regrouping is never “dirty”) |
any input / change | compare → toggle data-dirty, dispatch hc:dirtychange { dirty } on flips |
| tab close / reload while dirty | the browser’s generic prompt (custom strings are ignored by modern browsers) |
boosted <a> navigation while dirty | window.confirm with the dirtyguard.leave message (localize via setMessages()) |
| the form’s own request succeeds | re-snapshot → clean |
| a request from inside the form but not by it | not clean — the autosave draft stays a draft |
| native submit | never prompts (submit fires only after constraint validation passes) |
Styling the state
Section titled “Styling the state”data-dirty is a plain attribute — style it in app CSS:
.unsaved-badge { visibility: hidden; }form[data-dirty] .unsaved-badge { visibility: visible; }A server may also render data-dirty (a restored draft is unsaved
by definition) — the guard warns from the attribute alone.
Progressive enhancement
Section titled “Progressive enhancement”Without JavaScript nothing guards and nothing breaks: the form submits natively. Pure enhancement by construction.
Accessibility
Section titled “Accessibility”- The tab-close prompt is the browser’s own dialog.
- Mirror the state into an
aria-livestatus line viahc:dirtychangewhen a visual badge alone is not enough.
Related
Section titled “Related”- autosave — make the warning mostly moot by drafting as the user types.
- mutating-form — the save contract the guarded form usually follows.