Patterns / ConfirmationModal
ConfirmationModal
The confirmation modal asks the reader to confirm one action before it happens. It states the consequence in a line and offers a way out beside the action itself, so something that cannot easily be undone is never a single unguarded click.
The other pages in this section show compositions your own markup makes. This one the package ships. ConfirmationModal is a module: components arranged in one fixed way and published as a single export, imported exactly the way Button is.
Inside it is a compact Modal, whatever you pass as children, and a footer holding two Buttons. Nothing you could not have assembled yourself — the point is that every confirmation in every product is now the same one, down to which side Cancel sits on.
Three cases, in order: the everyday decision, the destructive one, and the one worth knowing about — a destructive action that is not a deletion. That third keeps the red button and renames it through confirmLabel, and passes no icon, since the glyph is optional.
When to use
Reach for it where one click does something the reader would want back — a deletion, a revocation, a send that reaches everyone — and the dialog buys them the moment it takes to read what it will cost. Ask rarely, though. A confirmation that fires on every save is clicked through without being read, and the habit of clicking through is formed long before something is genuinely at stake.
When not to use
A confirmation is one question with two answers, so anything carrying a field to fill in, a third option or a second step is a Modal — which is what this is built out of — and the tell is whether Cancel and the action really are the only two ways out. Never open one over another: a decision that takes two dialogs is a flow, and a flow is one Modal with a back step. And where the action can be undone, let it happen and say how to get back, because a gate in front of something reversible asks the reader to be sure about a risk they were never taking.
tone
Tone is the whole of the difference between the two confirmations, and it moves the label and the button together — there is no arrangement of props that puts Confirm on a red button.
| tone | When to use |
|---|---|
default | Confirm, on a primary button. For a decision worth stopping to make that leaves nothing behind it: publish, send, sign out everywhere. |
destructive | Delete, on a danger button. For an action that removes something the reader cannot get back. |
A destructive action that is not a deletion
confirmLabel renames the button and leaves its colour alone, which is the one place the pairing tone fixes comes apart — and only ever in that direction. Revoke, Discard, Sign out everywhere: the label follows what the action does while the red follows what it costs, so reach for this wherever Delete would be a lie about the button.
Without a glyph
The glyph is optional and the question reads perfectly well without one. Keep it where a family of confirmations wants a constant shape, and drop it where the drawing would only be decoration beside a sentence already doing the work.
Ask in the title and cost it in the body. "Delete Q3 pricing?" over "The model and its twelve saved scenarios go with it" — one is the question, the other is what the reader needs in order to answer it.
Name the verb on the button wherever you can. Confirm answers any question; Delete, Revoke and Discard answer this one, and someone reading only the button still knows what is about to happen.
Reserve destructive for what cannot be got back. If the record lands in a bin it can be pulled out of, the tone is default and the copy says where it went.
Cancel closes the dialog, and so do Escape, the backdrop and the close button; each of them reports through onOpenChange, so a controlled confirmation that ignores the event cannot be dismissed at all. The action is the one route out that closes nothing — it is yours to close on, once it lands.
The focus work is the dialog’s and this module adds nothing to it: focus moves in on open, the page behind stays inert, and focus returns to whatever opened it on close. Nothing here autofocuses either answer, which is worth leaving alone — an Enter pressed before the question has been read should not be able to answer it.
An action that can fail
onConfirm does not close the dialog, and that is deliberate: the modal is the last place a reader who has just committed to something is still looking, so it is the only place a failure can be reported to them. Close it yourself once the action lands, and where it does not, put what went wrong where the consequence was. A confirmation that closes optimistically leaves the reader on the page they started from with no idea whether anything happened.
One dialog for a list of rows
A table where every row can be deleted wants one confirmation with the row moved through it, not one created on demand. Keep the element mounted and drive open: a dialog unmounted the instant it closes has nothing left to animate out, so it disappears rather than closing, and disappearing is not a cue that the answer was taken.
Everything Modal takes, less width, onBack and backLabel — a confirmation is one screen at one size. The table below is the module's own props, plus the inherited ones a confirmation always sets.
| Prop | Type | Default | Notes |
|---|---|---|---|
tone | "default" | "destructive" | "default" | Picks the confirm button: Confirm on primary, Delete on danger. |
confirmLabel | string | the tone’s label | Replaces the label and leaves the button alone, for a destructive action that is not a deletion — "Revoke", "Discard". |
cancelLabel | string | "Cancel" | Label on the secondary button. |
onConfirm | () => void | — | The decision. It does not close the modal — an action that can fail should stay on screen to say so, so close it yourself once it lands. |
onCancel | () => void | — | Runs as the modal closes. Cancel closes it either way, through the dialog, so it works controlled or uncontrolled. |
title | string | — | Required. The question, and the dialog’s accessible name. Phrase it so that both buttons read as answers to it. |
icon | ReactNode | — | Glyph before the question, in the head. It takes the text colour at either tone — the danger colour belongs on the button, once. Omit it and the question starts the row. |
children | ReactNode | — | The consequence, in a sentence or two, centred under the title at 380px. The cost of the decision goes here; the title carries the question. |
open | boolean | — | Controlled open state. Pair it with onOpenChange. |
onOpenChange | (open: boolean) => void | — | Fires for every route out: Cancel, the backdrop, Escape and the close button. |
confirmation-modal | [data-module] | — | The dialog keeps Modal’s own data-slot, so this attribute sits beside it rather than replacing it — a rule written for every modal in an app still catches a confirmation, and this is how to catch only the confirmations. data-tone is mirrored next to it. |