a11y-dirty-form-guard
v1.0.0
Published
Accessible vanilla TypeScript controller for unsaved native form changes.
Maintainers
Readme
A11y Dirty Form Guard
Vanilla TypeScript controller that warns users when a watched form has unsaved changes.
The controller keeps native form semantics intact: it receives one form element directly, never wraps or moves controls, and uses the browser's native beforeunload warning only while the form is dirty.
Installation
npm install a11y-dirty-form-guard
pnpm add a11y-dirty-form-guard
yarn add a11y-dirty-form-guardUsage
Use a normal semantic form. Optionally provide an existing status element where you want clean and dirty messages to appear.
<p id="profile-status" hidden></p>
<form id="profile-form">
<label>
Name
<input name="name" value="Jane Doe" />
</label>
<button type="submit">Save</button>
<button type="reset">Reset</button>
</form>import { createDirtyFormGuard } from "a11y-dirty-form-guard";
const form = document.querySelector<HTMLFormElement>("#profile-form");
const statusElement = document.querySelector<HTMLElement>("#profile-status");
if (form) {
const guard = createDirtyFormGuard(form, {
statusElement: statusElement ?? undefined,
message: "Save or reset this form before leaving the page.",
disableOnSubmit: true
});
}For AJAX saves, call markClean() only after the save succeeds. Call destroy() when the form or its page section is removed.
guard.markClean();
guard.destroy();Options
interface DirtyFormGuardOptions {
message?: string;
enabled?: boolean;
ignore?: readonly string[];
disableOnSubmit?: boolean;
statusElement?: HTMLElement;
onDirtyChange?: (detail: DirtyFormGuardEventDetail) => void;
onDirty?: (detail: DirtyFormGuardEventDetail) => void;
onClean?: (detail: DirtyFormGuardEventDetail) => void;
}message: dirty-state text appended afterUnsaved changes:.enabled: starts browser leave protection enabled or disabled; defaults totrue.ignore: control names or IDs to skip.disableOnSubmit: disables leave protection when the form emits nativesubmit.statusElement: an existing element the controller may update; omit it to render no status UI.onDirtyChange: runs when dirty state or the changed-field set changes.onDirtyandonClean: run only when dirty state transitions.
Controls with data-dirty-ignore are skipped. Controls associated with the watched form using a matching native form attribute are included without moving or wrapping the form.
Controller API
interface DirtyFormGuard {
isDirty(): boolean;
markClean(): void;
markDirty(): void;
resetSnapshot(): void;
enable(): void;
disable(): void;
destroy(): void;
}Callback details have this shape:
{
instance: DirtyFormGuard;
dirty: boolean;
form: HTMLFormElement;
changedFields: readonly string[];
reason: "input" | "change" | "reset" | "mark-clean" | "mark-dirty" | "reset-snapshot";
}onDirtyChange does not run for repeated edits within the same already-dirty field. onDirty and onClean run only for transitions.
The package keeps one live controller per form. Repeating createDirtyFormGuard(form, options) returns the existing controller and keeps its first options; call destroy() before creating a replacement.
Keyboard and focus
The controller keeps native keyboard behavior: use Tab and Shift+Tab to move through the form in its normal order, and use the controls' standard keys to edit, select, submit, or reset. Dirty-state changes never move focus; the supplied status element is updated only when the form becomes dirty or clean.
Accessibility notes
- The watched form remains native HTML; labels, focus order, and submission semantics are not modified.
- The optional status uses text such as
All changes saved.andUnsaved changes:; it does not rely on color alone. - The controller applies
aria-live="polite"only for dirty/clean transitions, so routine keystrokes are not announced. - The controller restores the status element's original text and ARIA attributes when destroyed.
- The package does not create a custom dialog. Browsers do not allow custom text in the native close/refresh warning dialog.
- On form reset, dirty state is checked in
requestAnimationFrameso browser reset values are applied first. - File inputs are compared by basic file metadata: name and size.
Examples
- Basic: native controls, ignored fields, reset, submit, and an AJAX-style save.
- Team Settings: SaaS workspace settings demo with visible status and an ignored preview control.
- Manual accessibility QA scenarios: repeatable keyboard, screen-reader, reflow, forced-colors, and refresh-protection checks.
