Anatomy
┌──────────────────────────────────────────────┐
│ Invoice INV-1041 ● Unsaved changes │ ← continuous, quiet
│ …fields… [ Save ] │
└──────────────────────────────────────────────┘
on navigate away:
┌────────────────────────────────────────┐
│ Save your changes to INV-1041? │
│ 3 fields have been edited. │
│ [Cancel] [Discard] [Save and leave] │ ← three options, not two
└────────────────────────────────────────┘
Three obligations:
- Real dirty tracking. Compare against the loaded values. A guard that fires because someone clicked into a field is the fastest way to teach people to ignore it.
- A continuous indicator. The dialog should be a backstop, not the announcement. If the only time you learn about unsaved work is when you try to leave, the screen has been keeping a secret.
- Three choices at the interception. Save-and-continue is the one most often missing, and it is the one people want most of the time.
Why it works
It converts an invisible risk into a visible state. Work-in-progress is real to the person and invisible to the software unless someone chooses to model it — and once it is modelled, the indicator, the guard and the beforeunload handler all fall out of the same flag.
Naming the count matters more than it looks. "You have unsaved changes" is a category; "3 unsaved changes to this invoice" is a fact, and people make different decisions about facts.
The better fix, where it is available
A guard is a mitigation for a design where work can be lost. Where the domain allows it, autosave removes the problem instead of managing it, and the screen then owes a save-state indicator rather than a dialog.
The honest test is how much a person would lose. Drafting a long note: autosave. A financial record where a half-entered state is meaningless or dangerous: explicit save, with a guard.
Getting it wrong
- False positives. Firing on focus, on a value typed and undone, or on a field the system itself populated. Each one costs credibility, and credibility is the only thing making the dialog work.
- Two buttons. Cancel and Discard, with no way to save, is a dialog that punishes the person for having done the right thing.
- Only the browser warning. The generic beforeunload text cannot say what is at stake, and it does not fire on in-app navigation at all.
- Warning after a successful save, which happens whenever the flag is set on input but never cleared on response.
- Blocking a genuine escape. Someone must always be able to leave. The guard asks; it does not trap.
Exemplars
Google Docs is the strongest argument for the alternative: it removed the entire category by autosaving, and replaced the guard with a quiet "All changes saved" indicator — an idea now so normal that its absence feels like a defect.
GitHub's pull request comment box preserves drafts across navigation instead of guarding them, which is the third option: neither block nor lose, just keep.
Figma shows the save-state indicator done as a continuous, low-attention element rather than an interruption.
The extractable rule: a guard is what you build when you have decided work can be lost. Before building it, check whether that decision was necessary — the best version of this pattern is the one you did not need.