Pattern
Confirmation dialog
Asking someone to confirm before something irreversible happens. Confirmation is a cost paid by every user to protect against a mistake a few will make — so it has to be spent carefully, and it has to actually inform.
The patternPermalink to this section
Trigger the dialog and read it: the title names the object, the description names the consequence, and both buttons name their outcome.
StructurePermalink to this section
Four decisions, each of which changes whether the dialog is read or dismissed reflexively.
- Title — names the specific object: “Delete “Q3 marketing site”?”, not “Delete workspace?”.
- Description — states what is lost, with a count, and whether it can be undone.
- Cancel — labelled with what it preserves: “Keep workspace”. It is the safe default and gets initial focus.
- Confirm — repeats the verb from the trigger: “Delete workspace”. Destructive variant, right-hand side.
When confirmation is the wrong answerPermalink to this section
Most actions do not need a dialog. Undo is almost always better: it costs nothing when the action was intended, and fixes it completely when it was not.
Prefer undo to confirmation
Reach for a confirmation dialog only when the action is genuinely irreversible, affects other people, or costs money. Everything else should just happen, with an undo affordance.
High-consequence actionsPermalink to this section
For actions affecting live systems or other people, require the name to be typed. The friction is the point — it makes accidental confirmation impossible.
Use this sparingly
Type-to-confirm is the strongest tool available. Applied to routine actions it becomes a ritual people copy-paste through, and then it protects nothing.
Focus managementPermalink to this section
Where focus lands decides what happens when someone presses Enter out of habit.
- Focus starts on the cancelling action, so a reflexive Enter preserves rather than destroys.
- Focus is trapped inside the dialog — Tab never reaches the page behind it.
- Escape closes the dialog and cancels, returning focus to the trigger.
- After a successful destructive action, move focus to a stable element — the list heading, not the row that no longer exists.
- Never auto-focus the destructive button, however clearly it is labelled.
- For type-to-confirm, focus the input; the confirm button stays disabled until the text matches.
Do and don'tPermalink to this section
Do — name the outcome on both buttons
Buttons that make sense read alone survive the way people actually scan a dialog.
Don't — use Yes and No
“Yes” only means something if the title was read carefully, which is exactly what cannot be assumed.
Delete “Q3 marketing site”?
Removes 12 pages, 4 members and all history.
Do — quantify the loss
Concrete numbers turn an abstract warning into a decision someone can actually make.
Archive this project?
You can unarchive it later.
Don't — confirm reversible actions
If the description has to reassure that it is undoable, the dialog should not exist. Archive it and offer undo.
Code examplePermalink to this section
"use client";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@borneo/react";
import { Trash2 } from "lucide-react";
import { useState } from "react";
export function ConfirmDelete({ workspace, onDelete }) {
const [open, setOpen] = useState(false);
const [pending, setPending] = useState(false);
async function confirm() {
setPending(true);
await onDelete(workspace.id);
setPending(false);
setOpen(false);
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="destructive" leadingIcon={<Trash2 />}>
Delete workspace
</Button>
</DialogTrigger>
<DialogContent size="sm">
<DialogHeader>
{/* Names the object, not just the action */}
<DialogTitle>Delete “{workspace.name}”?</DialogTitle>
<DialogDescription>
This permanently removes {workspace.pageCount} pages,{" "}
{workspace.memberCount} members and all deployment history.
It cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
{/* Cancel is first in the DOM, so it receives initial focus */}
<DialogClose asChild>
<Button variant="ghost">Keep workspace</Button>
</DialogClose>
<Button variant="destructive" loading={pending} onClick={confirm}>
Delete workspace
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}Responsive behaviorPermalink to this section
Below 640px the dialog becomes a bottom sheet and the buttons stack, with the confirming action on top and cancel beneath it — both within thumb reach, and in the order people read.
AccessibilityPermalink to this section
- The dialog is role="dialog" with aria-modal, named by its title through aria-labelledby.
- The description is linked with aria-describedby, so the consequence is announced with the title.
- Focus is trapped while open and returns to the trigger on close, whatever closed it.
- Initial focus is on the cancelling action — the safe default.
- Escape cancels, matching what the close button and the overlay do.
- Destructive intent is carried by the button's label as well as its colour.
- After the action completes, announce the result in a live region so it is not silent for screen reader users.