Browse docs
Browse docs
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.
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>;
}Typically at the app root, inside your theme provider:
import { DashforgeTailwindProvider, ConfirmDialogProvider } from '@dashforge/tw-theme';
import { ConfirmDialogProvider as Provider } from '@dashforge/tw';
createRoot(document.getElementById('root')!).render(
<DashforgeTailwindProvider>
<Provider>
<App />
</Provider>
</DashforgeTailwindProvider>
);useConfirm() from any componentimport { 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();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>{/* 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.
await confirm({
title: 'Save and close?',
confirmLabel: 'Save & close',
cancelLabel: 'Keep editing',
});For "force a decision" flows (rare):
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.
body accepts ReactNode, not just strings:
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',
});Set common defaults at the provider level — useful for branding severity colors or default labels:
<ConfirmDialogProvider defaults={{ confirmLabel: 'Yes', cancelLabel: 'No' }}>
<App />
</ConfirmDialogProvider>Per-call options always override the provider defaults.
Configure <ConfirmDialogProvider> defaults application-wide.
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):
'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.
<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 ConfirmFntype 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;
};<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. |
<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.
<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.confirm() twice in a row, the second call waits for the first to resolve. Prevents stacked dialogs (which screen readers hate).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.<dialog> element only on the client (effect-only). SSR-safe — won't break hydration.<dialog> + showModal() are supported in every browser since 2022 (~98% coverage). If you need IE11 or older Safari, ConfirmDialog isn't for you.