Feedback
Confirm Dialog
One confirmation dialog for every destructive action — async pending, inline failure and type-to-confirm.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/confirm-dialog.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "ConfirmDialog" component on top of the
shadcn/Radix AlertDialog and Button primitives (do not hand-roll a modal —
AlertDialog already owns the overlay, focus trap and dismiss layer).
Contract
- Props: open?: boolean and onOpenChange?: (open: boolean) => void (controlled);
title: string; description?: ReactNode (rendered inside a <p>, so keep it to
text and inline elements); confirmText?: string (default "Confirm");
cancelText?: string (default "Cancel"); destructive?: boolean (default false);
onConfirm: () => void | Promise<void>; confirmPhrase?: string;
pendingText?: string (default "Working…"); trigger?: ReactNode;
className merged onto the dialog panel via cn().
- Dual mode: when `open` is omitted the component keeps its own open state and
`trigger` is rendered inside AlertDialogTrigger asChild; when `open` is passed
it is fully controlled and onOpenChange is the only way it closes.
Behavior
- onConfirm returning nothing: close immediately.
- onConfirm returning a Promise: enter a pending state — the confirm button is
disabled and swaps its label for pendingText next to a spinning loader, the
cancel button is disabled, and the dialog stays open. On resolve, close. On
reject, stay open, leave pending, and render the rejection message inline in a
destructive-tinted paragraph with role="alert" so the user can retry or cancel.
A synchronously thrown error takes the same path.
- While pending, every close request is swallowed: onOpenChange requests are
ignored and onEscapeKeyDown is preventDefault()-ed. (Radix AlertDialog already
blocks outside pointer interaction.) A half-finished mutation must never be
hidden behind a closed dialog.
- confirmPhrase: render a labelled text input ("Type <phrase> to confirm") and
keep the confirm button disabled until the value matches the phrase exactly
(case sensitive, no trimming). Enter inside the input takes the same confirm
path as the button.
- Every open starts clean: the typed phrase and any previous error reset when
`open` flips to true. Compare the prop against a state mirror during render
rather than in an effect, so the first painted frame is already clean.
Rendering & styling
- Semantic tokens only: the panel comes from AlertDialogContent (bg-popover),
the inline error is bg-destructive/10 + text-destructive, the phrase input is
border-input + focus-visible:ring-ring, hints are text-muted-foreground.
- Accessibility: the cancel button is a real AlertDialogCancel, which is where
Radix parks focus on open — the safe action is the default reach. The confirm
button is deliberately a plain <Button variant="destructive"> and NOT
AlertDialogAction: Action closes the dialog on click, which makes pending
states and disabled gating impossible. Title and description are wired to the
dialog through AlertDialogTitle / AlertDialogDescription. The loader icon is
aria-hidden and motion-reduce:animate-none.
Customization levers
- Severity: `destructive` only swaps the confirm button variant — leave it off
for reversible actions like "Archive" and keep the same component.
- Friction: `confirmPhrase` is the entire type-to-confirm feature; drop it for a
one-click confirm, or feed it the resource's own name for account-level
deletes. Exact matching lives in one comparison if you want it lenient.
- Labels: confirmText / cancelText / pendingText carry all the wording; keep
them verbs ("Delete project", "Deleting…") rather than "OK".
- Trigger: pass any element as `trigger` for the self-contained mode, or omit it
and drive `open` from a row action menu, a keyboard shortcut or route state.
- Layout: className lands on the dialog panel, so max-width and padding are
adjustable per instance; add an AlertDialogMedia icon slot inside the header
if your design language leads with an icon.Concepts
- Confirmation as a gate, not a notice — this dialog owns the moment before the action; anything that reports what already happened belongs in a toast instead.
- Type-to-confirm — asking for the resource name to be retyped converts muscle memory into deliberate intent, which is why the friction is reserved for account- or project-level deletes.
- Pending without closing — a destructive mutation that can fail must keep its dialog on screen while it runs, so the failure has somewhere to land and the retry costs no navigation.
- Bypassing AlertDialogAction — Radix's Action element closes the dialog on click by design, which forecloses
disabled, loading labels and failure states; a plain button on the same handler keeps the semantics and gains the state machine. - Focus on the safe action — Radix parks initial focus on Cancel, so a stray Enter dismisses instead of destroys, and the confirm button is always a deliberate second reach.
- Fresh state per open — resetting the typed phrase and error on each open stops a stale "still referenced by 2 deployments" message from greeting the next open.