Patterns

Destructive action

The playbook for anything a user can't take back. Delete, revoke, purge — dangerous controls get the solid critical color, an icon right next to it, and friction that matches what it destroys — a quick confirm for one record, a typed name for a whole scope, and an Undo toast instead of a dialog when nothing is really lost.

Principles

  • Critical color, never alone. A destructive control takes the solid critical fill — the metal name (rust) belongs to the proposed system — and per ADR-0017 the color always travels with an icon (octagon-alert, trash) and an explicit verb, so severity survives grayscale.
  • Never one-click. An irreversible action always confirms through a ConfirmDialog (an alertdialog). The one exception: skip the dialog when an undo window genuinely restores everything.
  • The cost is named. The confirm body says what is destroyed and what survives — never a bare "Are you sure?".
  • No danger zone. The record doctrine subsumes it: delete is a critical intent in the ActionBar beside the other intents, gated by its dialog — not a separate dread section at the bottom of the page.

The standard gate

One record, no take-backs: the critical intent in the bar pops a ConfirmDialog that tells you exactly what it costs. Cancel is the easy out; the confirm carries the solid critical fill (the rust metal is the proposed system's treatment).

Type to confirm

When it's bigger than one record — a bulk delete, a whole rulebook — the gate steps up: the confirm stays locked until you type the name of the thing you're deleting. Reading the name is the point; the button only unlocks on an exact match.

The undo exception

Archive, suppress, unassign — when the operation is fully recoverable, a dialog is the wrong friction. Act immediately and offer Undo in the toast; the window is the safeguard. Never offer undo as cover for actual data loss.

The escalation ladder

Friction is proportional to blast radius. Each tier adds to the one below; pick the lowest tier that honestly covers the loss.

tier 0 — staged
The commit doctrine itself: nothing autosaves, so most mistakes die at Discard before they ever need a gate.
tier 1 — confirm
Irreversible, one record: a critical ConfirmDialog that names the cost. The floor for any real deletion.
tier 2 — type to confirm
Irreversible at scope: bulk deletes, a whole rulebook, anything that fans out. The exact name is typed before the confirm unlocks.
the exception — undo
Recoverable operations skip the dialog: act now, toast with Undo. The contract is a real restore, not a fig leaf.

Placement

Where the destructive control sits is part of the safety.

in a record
Delete is a critical intent in the ActionBar, beside the other intents and gated by its dialog. There is no Danger Zone section; the commit doctrine subsumed it.
in a toolbar
Destructive bulk actions sit rightmost, separated from constructive controls by whitespace — and escalate to tier 2 when the selection fans out.
in a menu
Destructive items go last, below a divider, in the critical register with their glyph.

Accessibility

  • An alertdialog, by role. Both gates render role=alertdialog — focus is trapped, Escape backs out safely, and focus returns to the trigger (React Aria's wiring, never hand-rolled).
  • Severity is never colour alone. The critical color always pairs with an icon and an explicit verb (ADR-0017) — grayscale and color-blind users read the same danger.
  • Backing out is the easy path. Cancel is a plain button, Escape works, the scrim clicks away; only the confirm carries the critical weight.
  • Type-to-confirm stays honest. The field is a real labelled Input; paste is allowed deliberately (the friction is reading the name, not typing it), and the confirm is a real disabled→enabled button, never a hidden one.

Usage

Tier 1 — a critical intent and its gatetsx
import { ActionBar, ConfirmDialog } from '@halo-compliance/ui'
import { Check, Trash2 } from 'lucide-react'

<ActionBar
  dirty={dirty}
  intents={[
    { id: 'discard', label: 'Discard', tone: 'ghost',
      disabled: !dirty, onPress: form.reset },
    { id: 'delete', label: 'Delete report', tone: 'critical',
      icon: <Trash2 />, onPress: () => setConfirmOpen(true) },
    { id: 'save', label: 'Save changes', tone: 'primary',
      icon: <Check />, disabled: !dirty, onPress: form.handleSubmit },
  ]}
/>

<ConfirmDialog
  isOpen={confirmOpen}
  onOpenChange={setConfirmOpen}
  tone="critical"
  title="Delete this report?"
  confirmLabel="Delete report"
  cancelLabel="Cancel"
  onConfirm={deleteReport}
>
  This permanently removes the report and its findings. It can't be undone.
</ConfirmDialog>
Tier 2 — type-to-confirm over Modaltsx
import { Button, Input, Modal } from '@halo-compliance/ui'
import { Trash2 } from 'lucide-react'

const match = typed.trim() === rulebook.name // exact match unlocks

<Modal
  isOpen={open}
  onOpenChange={setOpen}
  role="alertdialog"
  size="sm"
  title={`Delete the ${rulebook.name} rulebook?`}
  footer={
    <>
      <Button variant="ghost" tone="secondary" onClick={close}>Cancel</Button>
      <Button variant="critical" icon={<Trash2 />} disabled={!match}
        onClick={deleteRulebook}>
        Delete rulebook
      </Button>
    </>
  }
>
  {/* name the cost, then the labelled type-to-confirm field */}
  <Input value={typed} onChange={(e) => setTyped(e.target.value)}
    valid={match} autoComplete="off" spellCheck={false} />
</Modal>

Anti-patterns

  • One-click destruction. A bare critical button that fires the API is never acceptable — even with the staged doctrine, irreversible confirms.
  • "Are you sure?" A confirm that names no cost teaches users to click through. Say what is destroyed.
  • Friction inflation. Type-to-confirm on an everyday single-record delete trains contempt for the gate; save it for scope.
  • Undo as a fig leaf. Offering Undo on a hard delete the backend can't restore is dishonest; the toast contract is a real restore.
  • /action-bar — the commit bar a critical intent lives in.
  • /modal — Modal and the ConfirmDialog convenience, the gates themselves.
  • /button — the solid critical fill the control wears.
  • /toast — the undo window's vehicle.