Feedback

Confirm Dialog

One confirmation dialog for every destructive action — async pending, inline failure and type-to-confirm.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { LoaderCircle } from "lucide-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  AlertDialog,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/confirm-dialog.json

Prompt

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.

On This Page