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 / Check by default, overridable via confirmIcon.

Usage

A delete intent, gatedtsx
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.
  • /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.