Confirm
An alert dialog with a trigger, a verb, and a pending state. The trigger is optional when open is controlled.
Confirm asks before an action that is hard to undo. It is a composition of
Alert dialog and
Button rather than a new overlay, so you pass a trigger,
a verb, and optionally pending. The dialog has no close button and ignores clicks outside
it, and Action and Cancel are kit Buttons. What sets it apart is the pending verb: the
confirm button shows its spinner, Cancel is disabled, Escape is locked, and the dialog is
marked busy until your handler settles.
Usage
import { Button } from '@/components/ui/button';
import { Confirm } from '@/components/ui/confirm';
<Confirm
trigger={<Button variant="destructive">Delete</Button>}
title="Delete this look?"
description="This cannot be undone."
verb="Delete"
variant="destructive"
pending={isDeleting}
onConfirm={() => deleteLook()}
/>Pending and disabled
Return a promise from onConfirm and the verb pends until it settles. The dialog stays open
if the promise rejects, so you can show the error and let the person try again. Pass pending
to drive the same state from outside, and it combines with a promise that is still running.
disabled disables the trigger.
Without a trigger
Leave out trigger when something else opens the dialog, such as a
Menu item or a keyboard shortcut. open and onOpenChange
are then required. On close, focus goes back to whatever held it when the dialog opened, if
that is still on the page. A menu item is gone by then, so pass the menu's trigger as
returnFocusRef.
const [open, setOpen] = React.useState(false);
const menuTriggerRef = React.useRef<HTMLButtonElement>(null);
<Menu>
<MenuTrigger asChild>
<IconButton ref={menuTriggerRef} icon={DotsThreeVertical} label="Actions" />
</MenuTrigger>
<MenuContent>
<MenuItem destructive onSelect={() => setOpen(true)}>Cancel send</MenuItem>
</MenuContent>
</Menu>
<Confirm
open={open}
onOpenChange={setOpen}
returnFocusRef={menuTriggerRef}
title="Cancel this send?"
verb="Cancel send"
variant="destructive"
onConfirm={cancelSend}
/>Props
| Prop | Type | Default | What it does |
|---|---|---|---|
trigger | ReactElement | The single element that opens the dialog. Omit it when open is controlled. | |
title | ReactNode | The dialog title, which is its accessible name. | |
description | ReactNode | Supporting copy under the title. | |
verb | ReactNode | The label on the confirm button. | |
cancelLabel | ReactNode | "Cancel" | The label on the cancel button. |
variant | "default" | "destructive" | "default" | The weight of the confirm button. |
pending | boolean | false | Holds the pending state from outside, combined with a running onConfirm. |
disabled | boolean | false | Disables the trigger. |
onConfirm | () => void | Promise<void> | Runs on confirm. Return a promise to pend the verb. | |
open | boolean | Controls the open state. Required without trigger. | |
defaultOpen | boolean | false | The initial open state when uncontrolled. Needs trigger. |
onOpenChange | (open: boolean) => void | Called when the open state changes. Required without trigger. | |
returnFocusRef | RefObject<HTMLElement | null> | Without trigger, where focus lands on close. |
Accessibility
- The dialog has
role="alertdialog"andaria-modalfrom Radix Alert dialog, the title names it, and the description describes it. - Focus moves into the dialog on open, and it returns to the trigger on close. Without a
trigger it returns to
returnFocusRef, or to whatever held focus when the dialog opened. - Escape closes the dialog unless the verb is pending, Tab stays inside it, and a click outside does not dismiss it.
- While pending the dialog is
aria-busyand Cancel is disabled. The spinner is the visible sign, so keep the verb's label meaningful, because nothing else is announced when it starts. - Action and Cancel are Buttons, so they have the same
:focus-visiblering, the transparent outline in forced colors, and at least 44px of height on a touch screen. - Under reduced motion the dialog opens without its pop, and the spinner keeps turning.
When to use something else
- Alert dialog when the action needs no pending state, or when you want to compose the buttons yourself.
- Dialog when the person fills something in rather than answering yes or no.
- Sheet or Drawer for a longer task that should not block the page.
Install
shadcn add @gesso/confirm