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.
Theme defaults
Configure <ConfirmDialogProvider> defaults application-wide.
tsx
import { patchTheme } from '@dashforge/tw-theme';
patchTheme({
components: {
ConfirmDialog: {
defaults: {
severity: 'warning',
invocationDefaults: {
confirmLabel: 'Confirm',
cancelLabel: 'Cancel',
},
},
},
},
});
Configurable axes (ConfirmDialogVariantProps):
| Axis | Type | Notes |
|---|
severity | 'info' | 'warning' | 'danger' | 'success' | Drives the confirm button color across all invocations under the provider. |
invocationDefaults | ConfirmOptions | Theme-level base merged UNDER the provider's own defaults prop and the per-call confirm(options). |
children and slotProps are per-instance choices — not theme-configurable.
Precedence chain for severity (lowest → highest):
- Built-in default (
'info').
theme.components.ConfirmDialog.defaults.severity (application-wide).
<ConfirmDialogProvider severity="…" /> — wins over theme.
Precedence chain for per-call options (lowest → highest):
theme.components.ConfirmDialog.defaults.invocationDefaults (theme base for every call).
<ConfirmDialogProvider defaults={…} /> — wins over theme.
confirm(options) call — wins over both above.
TypeScript: theme.components.ConfirmDialog.defaults autocompletes to the two axes above.
Reactivity: useComponentDefaults('ConfirmDialog') subscribes to the theme store — patchTheme re-renders the provider with the new axes.
API
<ConfirmDialogProvider> props
| Prop | Type | Default | Description |
|---|
children | ReactNode | — | App tree to wrap. Required. |
defaults | ConfirmOptions | — | Default options merged into every confirm() invocation. |
severity | 'success' | 'warning' | 'danger' | 'info' | 'info' | Provider-wide confirm button intent — drives the color of the primary action across every confirm() invocation under this provider. |
slotProps | ConfirmDialogSlotProps | — | Per-slot className 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 & slotProps
<ConfirmDialogProvider> is a compound component with 7 named slots. Each slot accepts a { className?: string } override via slotProps — the override applies to every dialog rendered by the provider.
| Slot | Purpose |
|---|
backdrop | The ::backdrop pseudo-element (semi-opaque overlay). |
dialog | The modal box surface (bg + border + shadow). |
title | The heading at the top of the dialog. |
body | The message body container. |
actions | The bottom button row container. |
confirmButton | The primary action button (severity-colored). |
cancelButton | The secondary action button. |
tsx
<ConfirmDialogProvider
slotProps={{
dialog: { className: 'max-w-lg' },
confirmButton: { className: 'font-semibold' },
}}
>
<App />
</ConfirmDialogProvider>
For per-call variance (a specific dialog styled differently from the rest), pass slotProps inline to that confirm({ slotProps: … }) call.
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.