ConfirmDialog
An imperative confirmation modal. You don't render <ConfirmDialog> yourself — you mount the <ConfirmDialogProvider> once at app root and call await confirm({ ... }) from anywhere in your code. Returns Promise<boolean>: true if confirmed, false if cancelled (or Escape, or backdrop click).
Backed by the native <dialog> element + showModal() — focus trap, Escape handler, accessibility tree isolation all come free from the browser, no library hand-rolled approximation.
tsx
import { useConfirm } from '@dashforge/tw';
function DeleteButton({ itemId }: { itemId: string }) {
const confirm = useConfirm();
async function handleDelete() {
const ok = await confirm({
title: 'Delete this item?',
body: 'This action cannot be undone.',
severity: 'danger',
confirmLabel: 'Delete',
});
if (ok) await api.delete(itemId);
}
return <Button color="danger" onClick={handleDelete}>Delete</Button>;
}
Quick Start
1. Mount the provider once
Typically at the app root, inside your theme provider:
tsx
import { DashforgeTailwindProvider, ConfirmDialogProvider } from '@dashforge/tw-theme';
import { ConfirmDialogProvider as Provider } from '@dashforge/tw';
createRoot(document.getElementById('root')!).render(
<DashforgeTailwindProvider>
<Provider>
<App />
</Provider>
</DashforgeTailwindProvider>
);
2. Call useConfirm() from any component
tsx
import { useConfirm, Button } from '@dashforge/tw';
const confirm = useConfirm();
const ok = await confirm({
title: 'Log out?',
body: 'You will need to sign in again to continue.',
});
if (ok) signOut();
Examples
import { ConfirmDialogProvider, useConfirm, Button, Stack } from '@dashforge/tw';
function MyApp() {
const confirm = useConfirm();
return (
<Stack direction="row" gap={2}>
<Button
color="primary"
onClick={async () => {
await confirm({
title: 'Save changes?',
body: 'Your edits will be applied to the workspace.',
severity: 'info',
confirmLabel: 'Save',
});
}}
>
Save changes
</Button>
<Button
color="danger"
onClick={async () => {
await confirm({
title: 'Delete workspace?',
body: 'This will permanently remove the workspace and all its data.',
severity: 'danger',
confirmLabel: 'Delete workspace',
});
}}
>
Delete workspace
</Button>
</Stack>
);
}
// Mount the provider once at app root:
<ConfirmDialogProvider>
<MyApp />
</ConfirmDialogProvider>
Severity variants
tsx
{/* info — neutral confirmation */}
await confirm({
title: 'Apply changes?',
body: 'Your edits will be saved to the workspace.',
severity: 'info',
confirmLabel: 'Apply',
});
{/* warning — caution, not destructive */}
await confirm({
title: 'Discard draft?',
body: 'Your unsaved changes will be lost.',
severity: 'warning',
});
{/* danger — destructive, irreversible */}
await confirm({
title: 'Delete workspace?',
body: 'This will permanently remove the workspace and all its data.',
severity: 'danger',
confirmLabel: 'Delete workspace',
});
{/* success — positive confirmation (rare but exists) */}
await confirm({
title: 'Publish?',
body: 'The article will be visible to everyone.',
severity: 'success',
confirmLabel: 'Publish now',
});
severity drives the confirm button's color. Default is info.
Custom labels
tsx
await confirm({
title: 'Save and close?',
confirmLabel: 'Save & close',
cancelLabel: 'Keep editing',
});
Disable Escape / backdrop close
For "force a decision" flows (rare):
tsx
await confirm({
title: 'Read the terms first',
body: 'You must accept or decline before continuing.',
disableEscapeClose: true,
disableBackdropClose: true,
confirmLabel: 'Accept',
cancelLabel: 'Decline',
});
Use sparingly — most users expect Escape to dismiss modals.
Rich body content
body accepts ReactNode, not just strings:
tsx
await confirm({
title: 'Transfer ownership',
body: (
<Stack gap={3}>
<Typography>You're about to transfer this workspace to:</Typography>
<Box variant="outlined" p={4}>
<Stack direction="row" align="center" gap={3}>
<Avatar src={newOwner.avatar} />
<Stack gap={0}>
<Typography variant="body1">{newOwner.name}</Typography>
<Typography variant="caption" color="muted">{newOwner.email}</Typography>
</Stack>
</Stack>
</Box>
<Typography variant="caption" color="warning">
You will lose admin access to this workspace.
</Typography>
</Stack>
),
severity: 'warning',
confirmLabel: 'Transfer',
});
Provider defaults
Set common defaults at the provider level — useful for branding severity colors or default labels:
tsx
<ConfirmDialogProvider defaults={{ confirmLabel: 'Yes', cancelLabel: 'No' }}>
<App />
</ConfirmDialogProvider>
Per-call options always override the provider defaults.
API
<ConfirmDialogProvider> props
| Prop | Type | Default | Description |
|---|
children | ReactNode | — | App tree to wrap. Required. |
defaults | ConfirmOptions | — | Default options applied to every confirm() call. |
slotProps | ConfirmDialogSlotProps | — | Per-slot overrides applied to every dialog. |
useConfirm() returns ConfirmFn
ts
type ConfirmFn = (options: ConfirmOptions) => Promise<boolean>;
type ConfirmOptions = {
title: ReactNode;
body?: ReactNode;
confirmLabel?: string; // default 'Confirm'
cancelLabel?: string; // default 'Cancel'
severity?: 'info' | 'warning' | 'danger' | 'success'; // default 'info'
disableBackdropClose?: boolean;
disableEscapeClose?: boolean;
slotProps?: ConfirmDialogSlotProps;
};
Slots
backdrop · dialog · title · body · actions · confirmButton · cancelButton
Notes
- Native
<dialog> element: focus trap, Escape close, accessibility tree isolation come free from the browser via showModal(). No need for react-aria or a hand-rolled modal helper — the platform does it AAA-grade out of the box.
- One-at-a-time queue: if you call
confirm() twice in a row, the second call waits for the first to resolve. Prevents stacked dialogs (which screen readers hate).
- Promise resolves to
false on Escape, backdrop click (when not disabled), or Cancel button. Resolves to true on Confirm button. No "indeterminate" state — the user always makes a binary choice or dismisses.
- For non-binary choices (3+ options like "Save / Discard / Cancel"): build a custom modal, ConfirmDialog is intentionally binary. The "save dialog with three buttons" is a different UX problem.
- Server-side rendering: the provider mounts a
<dialog> element only on the client (effect-only). SSR-safe — won't break hydration.
- Browser support:
<dialog> + showModal() are supported in every browser since 2022 (~98% coverage). If you need IE11 or older Safari, ConfirmDialog isn't for you.