Confirm Dialog
Asks the user to confirm an action before it runs, with one awaited call to confirm().
@zenoxai/ui/patterns/confirm-dialog MarkdownNot asked yet.
function ConfirmDialogDemo() {
const [result, setResult] = React.useState<boolean | null>(null)
async function dismissAlert() {
const confirmed = await confirm({
title: "Dismiss this alert?",
description:
"The alert moves to Dismissed and stops notifying the on-call analyst.",
confirmLabel: "Dismiss alert",
})
setResult(confirmed)
}
return (
<div className="flex flex-col items-center gap-3">
<Button variant="outline" onClick={dismissAlert}>
Dismiss alert
</Button>
<p className="text-sm text-text-secondary">
{result === null
? "Not asked yet."
: `confirm() resolved to ${result}.`}
</p>
</div>
)
}Usage
Mount one ConfirmDialogHost near the root of the app, next to the Toaster:
import { Toaster } from "@zenoxai/ui/components/sonner"
import { ConfirmDialogHost } from "@zenoxai/ui/patterns/confirm-dialog"
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<>
{children}
<Toaster />
<ConfirmDialogHost confirmLabel="Confirm" cancelLabel="Cancel" />
</>
)
}
Then call confirm() from any event handler. It opens the dialog and resolves to true when
the user confirms, false when they cancel, press Escape or onConfirm throws:
import { confirm } from "@zenoxai/ui/patterns/confirm-dialog"
async function dismissAlert() {
const confirmed = await confirm({
title: "Dismiss this alert?",
description:
"The alert moves to Dismissed and stops notifying the on-call analyst.",
confirmLabel: "Dismiss alert",
})
if (!confirmed) return
// ...
}
The host takes the default button labels, already translated. Everything else comes with each call. Mount exactly one host: every host renders the same dialog, so a second one opens a second copy on top of the first. This page mounts one host for all of its examples.
Ask for confirmation only before actions that are destructive, expensive or hard to undo. For an action that can be undone, run it and offer Undo in a toast. For a task that needs a form, use a Dialog.
Write the title as the question, naming the action and the object (“Delete tenant
acme-prod?”), and the description as what happens and whether it can be undone. Repeat the
verb of the title in confirmLabel (“Delete tenant”), never “OK” or “Yes”. The full list of
options is in the API reference below.
A second confirm() while a dialog is open cancels the first one, which resolves to false.
Examples
Destructive
Set confirmVariant: "destructive" for an action that deletes data or cannot be undone, and say
so in the description.
Not asked yet.
function ConfirmDialogDestructive() {
const [result, setResult] = React.useState<boolean | null>(null)
async function deleteReport() {
const confirmed = await confirm({
title: "Delete this report?",
description:
"Monthly exposure report, September 2026 will be deleted for everyone in the tenant. This cannot be undone.",
confirmLabel: "Delete report",
confirmVariant: "destructive",
})
setResult(confirmed)
}
return (
<div className="flex flex-col items-center gap-3">
<Button variant="destructive" onClick={deleteReport}>
Delete report
</Button>
<p className="text-sm text-text-secondary">
{result === null
? "Not asked yet."
: `confirm() resolved to ${result}.`}
</p>
</div>
)
}Type to confirm
For a destructive action on a named resource, such as a tenant or an integration, pass
inputField.expectedValue. The dialog shows the value after the field label as a monospace chip
on a muted fill, so it reads as the literal to copy (one click selects it whole). Write the
label as the instruction without the value, such as “Type the tenant name to confirm”. The
dialog renders a single-line field and keeps the confirm button disabled until the trimmed input equals the
value exactly; Enter then confirms. While onConfirm runs, the confirm button shows a spinner
and is marked busy.
Not asked yet.
function ConfirmDialogTypeToConfirm() {
const [result, setResult] = React.useState<boolean | null>(null)
async function deleteTenant() {
const confirmed = await confirm({
title: "Delete tenant acme-prod?",
description:
"Every asset, alert and report of this tenant will be deleted. This cannot be undone.",
confirmLabel: "Delete tenant",
confirmVariant: "destructive",
inputField: {
label: "Type the tenant name to confirm",
expectedValue: "acme-prod",
},
})
setResult(confirmed)
}
return (
<div className="flex flex-col items-center gap-3">
<Button variant="destructive" onClick={deleteTenant}>
Delete tenant
</Button>
<p className="text-sm text-text-secondary">
{result === null
? "Not asked yet."
: `confirm() resolved to ${result}.`}
</p>
</div>
)
}With an option
checkboxField adds one checkbox, for a choice that only makes sense at the moment of
confirming. The promise resolves to a boolean, so read the checkbox in onConfirm.
Not asked yet.
function ConfirmDialogWithCheckbox() {
const [message, setMessage] = React.useState("Not asked yet.")
async function removeAsset() {
let deleteHistory = false
const confirmed = await confirm({
title: "Remove web-prod-03?",
description: "The asset stops being scanned and leaves every report.",
confirmLabel: "Remove asset",
confirmVariant: "destructive",
checkboxField: { label: "Also delete its scan history" },
onConfirm: async ({ checked }) => {
deleteHistory = checked
},
})
if (!confirmed) setMessage("confirm() resolved to false.")
else if (deleteHistory) setMessage("Removed, with its scan history.")
else setMessage("Removed. Scan history kept.")
}
return (
<div className="flex flex-col items-center gap-3">
<Button variant="outline" onClick={removeAsset}>
Remove asset
</Button>
<p className="text-sm text-text-secondary">{message}</p>
</div>
)
}Async action
Run the action in onConfirm to keep the dialog open while it runs. Both buttons are disabled
and Escape is ignored until the promise settles. If onConfirm throws, the dialog closes and
confirm() resolves to false; report the error from inside onConfirm, for example with a
toast.
Not asked yet.
function ConfirmDialogAsync() {
const [result, setResult] = React.useState<boolean | null>(null)
async function revokeKey() {
const confirmed = await confirm({
title: "Revoke this API key?",
description:
"Integrations that use the key stop sending events until you add a new one.",
confirmLabel: "Revoke key",
confirmVariant: "destructive",
onConfirm: async () => {
// Stands in for the API call. Throw to keep the key and resolve false.
await new Promise((resolve) => setTimeout(resolve, 1500))
},
})
setResult(confirmed)
}
return (
<div className="flex flex-col items-center gap-3">
<Button variant="outline" onClick={revokeKey}>
Revoke API key
</Button>
<p className="text-sm text-text-secondary">
{result === null
? "Not asked yet."
: `confirm() resolved to ${result}.`}
</p>
</div>
)
}Accessibility
- The dialog is an alert dialog: it traps focus, does not close on an outside click, and on
close returns focus to the element that had it before, usually the button that called
confirm(). - The title and description are the dialog’s accessible name and description. Write both as full sentences; the title alone must make sense.
- While
onConfirmruns, the buttons are only disabled. If the action takes more than a moment, report progress or the result with a toast so screen reader users hear it.
API reference
Generated from the TypeScript source. className, style and children are accepted everywhere and omitted below.
ConfirmDialogHost
ConfirmDialogHost renders the dialog that `confirm()` opens. Mount it once, next to the Toaster, near the root of the app. It holds no props per call: every call passes its own options to `confirm()`.
| Prop | Type | Default |
|---|---|---|
cancelLabel | stringAlready-translated default for the cancel button. | Cancel |
confirmLabel | stringAlready-translated default for the confirm button. | Confirm |
useConfirmDialog
useConfirmDialog returns the `confirm()` function. It exists for symmetry with hooks-first code; calling `confirm` directly works the same.
No props of its own. Accepts the props of the element it renders.
confirm
Opens the app's confirmation dialog and resolves to true when the user confirms, false when they cancel or `onConfirm` throws. Requires one <ConfirmDialogHost /> mounted near the root of the app.
| Prop | Type | Default |
|---|---|---|
cancelLabel | string | - |
checkboxField | ConfirmCheckboxField | - |
confirmLabel | string | - |
confirmVariant | "link" | "default" | "destructive" | "outline" | "secondary" | "ghost"Variant of the confirm button. Use `destructive` for irreversible actions. | - |
description* | string | - |
inputField | ConfirmInputField | - |
onConfirm | ((input: { text: string; checked: boolean; }) => Promise<void>)Runs while the dialog shows a pending state. If it throws, the dialog closes and the promise resolves to false; report the error from here. | - |
title* | string | - |