Patterns
Confirmation
The quick "hold on" before something big. Most actions shouldn't need one: staged edits are free to toss, and a reversible action just happens, with undo waiting right there in the toast. Save the question for the clicks that really commit you: irreversible, expensive, or binding.
Three actions, three treatments
Archive can be undone, so it just happens — Undo waits in the toast. Delete is forever, so a critical confirmation steps in. Submit for audit isn't scary, but it locks things in, so a friendly confirmation checks first.
Confirmation or undo
- Reversible? Undo. If the action can be cleanly walked back — archive, a status change — perform it immediately and offer Undo in the toast. Asking first is friction with no payoff.
- Irreversible? Confirm. Delete, revoke, finalize, submit-for-audit — anything that cannot be walked back is gated by a ConfirmDialog that names the consequence.
- High-cost or wide? Confirm. A bulk operation across many records may be technically reversible and still worth a stop — name the blast radius ("This affects 240 records").
- Safe? Neither. Saving a setting or a draft needs no question. Confirmation theater teaches users to click through — and click-through is what the real confirmations can't survive.
A critical intent, not a danger zone
No scary red basement at the bottom of the page. Delete is just one more button in the ActionBar — painted for what it does, sitting right beside Save and Discard — and the ConfirmDialog brings the "hold on". The dialog, not the location, is what keeps accidents out.
Staged or live
Nothing here autosaves. Your edits stage up locally and only become real when you press an intent — and that line is what decides when we ask first.
- staged edits
- Free. Discarding staged work undoes something that never happened — no confirmation. The one exception: a large staged set may warrant a cautionary stop, because the typing is real even though the commit isn't.
- live commits
- Where consequence enters. A reversible commit runs and offers Undo; an irreversible or binding one is gated by confirmation before it runs.
Anatomy
- heading
- Names the action as a question — "Delete this report?" — never "Are you sure?".
- body
- One or two sentences: what will happen, and what it costs. If anything is irreversible, say so — "This cannot be undone" is load-bearing for the user's mental model.
- details
- Optional — what specifically is affected, when the blast radius isn't obvious (a count, a short list).
- confirm
- Reads the consequence, completing the headline — "Delete report", never "Yes" or "OK". Toned for severity, rightmost.
- cancel
- Always leftmost (the forms convention). Esc and the × are equivalent to it.
Severity
The confirm button wears the severity as a solid color fill, and per ADR-0017 color never stands alone — a glyph pairs with it. (Rust, copper, patina — those metal names belong to the proposed system; here the fills are plain, friendly solids.)
- destructive
- Solid red — the critical confirm. Delete, revoke. Octagon-alert (or the action's own glyph, like trash) pairs with the color, and the body carries the "cannot be undone" callout.
- cautionary
- Solid orange — recoverable but wide: bulk operations, a big staged discard. Triangle-alert pairs with it. ConfirmDialog doesn't expose this tone yet; compose Modal with a caution Button until a real flow pulls it in.
- affirmative
- The solid primary fill — binding but not dangerous: finalize, submit for audit. The default confirm; check pairs with it.
Accessibility
- An alertdialog, trapped. role=alertdialog via React Aria — focus is trapped inside, scroll is locked, and focus returns to the trigger on close. Never hand-rolled.
- Enter can't accidentally commit. Initial focus lands on a dismissing control, never on the confirm — confirming takes a deliberate move through the tab order.
- Esc is Cancel. Escape and the × dismiss without confirming, exactly like the Cancel button.
- Severity is never colour alone. The confirm icon and the body callout carry the meaning with a glyph (ADR-0017).
- Pending locks, not blinds. While the action runs, both buttons disable in place — the dialog doesn't vanish until the caller closes it.
API
- isOpen / onOpenChange
- Controlled, like the rest of the register. Closing after a successful confirm is the caller's job.
- title / children
- The question and the body — what's about to happen, and what it costs.
- confirmLabel / cancelLabel
- Translated strings. The confirm label reads the consequence ("Delete report").
- onConfirm / isPending
- onConfirm fires when the confirm is pressed; isPending locks both buttons while the action runs.
- tone / confirmIcon
- tone="critical" paints the confirm rust; the default confirms in primary gold. Metal buttons require an icon —
Trash2 / Checkby default, overridable via confirmIcon.
Usage
import { ConfirmDialog, toast } from '@halo-compliance/ui'
import { Trash2 } from 'lucide-react'
// the ActionBar's critical intent opens it…
{ id: 'delete', label: 'Delete', tone: 'critical',
icon: <Trash2 />, onPress: () => setConfirmOpen(true) }
// …and the dialog supplies the friction
<ConfirmDialog
isOpen={confirmOpen}
onOpenChange={setConfirmOpen}
tone="critical"
title="Delete this risk report?"
confirmLabel="Delete report"
cancelLabel="Cancel"
isPending={deletion.isPending}
onConfirm={async () => {
await deleteReport(report.id)
setConfirmOpen(false)
toast.affirm('Report deleted.')
}}
>
The report and its findings are removed for every member
of this organization. This cannot be undone.
</ConfirmDialog>Gaps
- No caution tone on ConfirmDialog yet. tone is default | critical. The copper cautionary treatment is composed from Modal + a caution Button until real bulk flows pull it into the component.
- No typed-name-to-confirm. The pattern for nuclear actions (type the record's name to arm the button) waits for an action that earns it.
- No batch recipe. Confirming a multi-record operation — the count, the list, partial failure — will be specified alongside the first real bulk flow.
Anti-patterns
- "Are you sure?" A heading that names nothing forces a re-read of the page. Name the action: "Revoke this access?".
- Yes / No / OK buttons. The confirm must read the consequence; "Yes" makes the user reconstruct what they're agreeing to.
- Confirming instead of undo. If you can offer Undo, asking first is the worse experience and the weaker safety net.
- A danger zone. Quarantining delete in a red region swaps friction for geography. The intent belongs in the ActionBar; the dialog is the gate.
- Stacked confirmations. One question per action. A second "really?" dialog signals the first one wasn't doing its job.
Related
- /modal — the overlay ConfirmDialog is built on; reach for it when the decision needs more than yes/no.
- /action-bar — where the critical intent lives; the commit doctrine in full.
- /toast — the Undo vehicle for reversible actions.
- /record — the work surface whose intents these are.