Confirm dialog
Asks once before an action that can't be undone or costs something. It says what will happen, and the button repeats the verb.
Playground
Press the button, then Cancel to come back to the controls. The code under the preview lists only the props you changed.
import { ConfirmDialog } from '@/ui/confirm-dialog';<ConfirmDialog title="Delete Orbital Freight?" description="The company and its notes go for good." action="Delete company" />Destructive
The danger action names what goes. Focus starts on Cancel, so Enter alone never deletes. There is no close button: Cancel and Escape are the way out.
import { useState } from 'react';
import { Button } from '@/ui/button';
import { ConfirmDialog } from '@/ui/confirm-dialog';
import { toast } from '@/ui/toast';
// Level 2: permanent, one record. Soft red action; focus starts on Cancel, so Enter alone never deletes.
export default function ConfirmDialogDefault() {
const [open, setOpen] = useState(false);
return (
<>
<Button variant="danger" onClick={() => setOpen(true)}>
Delete view
</Button>
<ConfirmDialog
open={open}
onOpenChange={setOpen}
title="Delete “Shortlist this week”?"
description="Everyone at Northbridge Talent loses this view. The candidates in it stay."
action="Delete view"
onAction={() => {
setOpen(false);
toast.add({ title: 'View deleted' });
}}
/>
</>
);
}Destructive levels
The ask scales with the damage. Pick the level by what can be undone and how far it reaches, never by how serious the button feels.
- Can be undone: no dialog. It happens, and a toast says what happened with Undo.
- Permanent, one record: this dialog with a soft red action.
- Permanent and wide (bulk delete, delete with related records, remove a member): type a phrase such as "delete 24"; the solid red action enables only when it matches. No hold-to-delete.
import { useState } from 'react';
import { Button } from '@/ui/button';
import { ConfirmDialog } from '@/ui/confirm-dialog';
import { toast } from '@/ui/toast';
// Decided 8: the ask scales with the damage. Level 1 acts at once with Undo; level 2 confirms with a soft red action;
// level 3 asks for a typed phrase before the solid red action enables.
export default function ConfirmDialogLevels() {
const [open, setOpen] = useState<2 | 3 | null>(null);
return (
<div className="flex flex-wrap gap-100">
<Button onClick={() => toast.add({ title: 'Maya Chen archived', actionProps: { children: 'Undo' } })}>Archive Maya Chen</Button>
<Button variant="danger" onClick={() => setOpen(2)}>
Delete Orbital Freight
</Button>
<Button variant="danger-bold" onClick={() => setOpen(3)}>
Delete 24 candidates
</Button>
<ConfirmDialog
open={open === 2}
onOpenChange={(next) => !next && setOpen(null)}
title="Delete Orbital Freight?"
description="The company and its notes go for good. Its contacts and jobs stay, without a company."
action="Delete company"
onAction={() => setOpen(null)}
/>
<ConfirmDialog
open={open === 3}
onOpenChange={(next) => !next && setOpen(null)}
title="Delete 24 candidates?"
description="Their applications, notes and files go too. This can’t be undone."
action="Delete 24 candidates"
confirmPhrase="delete 24"
onAction={() => setOpen(null)}
/>
</div>
);
}Costly, and with a choice
A costly but expected action takes the primary button, and focus starts on it; the description counts what happens, with names. With a picker, the action stays disabled until the choice is made.
import { useState } from 'react';
import { Button } from '@/ui/button';
import { ConfirmDialog } from '@/ui/confirm-dialog';
import { Field, FieldLabel } from '@/ui/field';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/ui/select';
const reasons = { salary: 'Salary too high', notice: 'Notice period too long', fit: 'Not the right fit' };
// Costly but expected: the primary action, focus on it. With a picker, the action waits until a choice is made.
export default function ConfirmDialogCostly() {
const [open, setOpen] = useState<'send' | 'close' | null>(null);
const [reason, setReason] = useState<string | null>(null);
return (
<div className="flex flex-wrap gap-100">
<Button variant="primary" onClick={() => setOpen('send')}>
Send 3 CVs
</Button>
<Button onClick={() => setOpen('close')}>Close application</Button>
<ConfirmDialog
open={open === 'send'}
onOpenChange={(next) => !next && setOpen(null)}
appearance="primary"
title="Send 3 CVs to Halcyon Bank?"
description="Sara Lund gets one email with Maya Chen, Daniel Okafor and Priya Shah. Their stage moves to CV sent."
action="Send CVs"
onAction={() => setOpen(null)}
/>
<ConfirmDialog
open={open === 'close'}
onOpenChange={(next) => !next && setOpen(null)}
appearance="primary"
title="Close Maya Chen’s application?"
description="It leaves the board for Senior Data Engineer. You can reopen it later."
action="Close application"
disabled={!reason}
onAction={() => setOpen(null)}
>
<Field>
<FieldLabel id="close-reason-label">Reason</FieldLabel>
<Select items={reasons} value={reason} onValueChange={setReason}>
<SelectTrigger aria-labelledby="close-reason-label">
<SelectValue placeholder="Choose a reason" />
</SelectTrigger>
<SelectContent>
{Object.entries(reasons).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
</ConfirmDialog>
</div>
);
}Working and a failed action
While the action runs, its button shows a spinner and Cancel and Escape wait; a second press does nothing. When it fails, the dialog stays open and says what happened and what to do next. This one fails the first time. On success the dialog closes and a toast says what happened.
import { useState } from 'react';
import { Button } from '@/ui/button';
import { ConfirmDialog } from '@/ui/confirm-dialog';
// Working: a spinner in the action, Cancel and Escape wait, a second press does nothing. The first try fails: the dialog
// stays open and says what to do. The second works.
export default function ConfirmDialogUnhappy() {
const [open, setOpen] = useState(false);
const [pending, setPending] = useState(false);
const [error, setError] = useState<string | null>(null);
const [tries, setTries] = useState(0);
return (
<>
<Button variant="danger" onClick={() => setOpen(true)}>
Delete “Nguyễn Thị Minh Khai – Senior Data Engineer shortlist”
</Button>
<ConfirmDialog
open={open}
onOpenChange={(next) => {
setOpen(next);
if (!next) setError(null);
}}
title="Delete “Nguyễn Thị Minh Khai – Senior Data Engineer shortlist”?"
description="Everyone at Northbridge Talent loses this view. The candidates in it stay."
action="Delete view"
pending={pending}
error={error}
onAction={() => {
setPending(true);
setError(null);
setTimeout(() => {
setPending(false);
setTries(tries + 1);
if (tries === 0) setError('Couldn’t delete the view. Check your connection and try again.');
else setOpen(false);
}, 1200);
}}
/>
</>
);
}Motion
The confirm dialog uses the Dialog motion. Press Replay to open it after a beat, then Cancel or Escape to see it close.
import { useState } from 'react';
import { Button } from '@/ui/button';
import { ConfirmDialog } from '@/ui/confirm-dialog';
// Replay opens the dialog after a beat, so the 200ms entry plays; Cancel or Escape plays the 150ms exit.
export default function ConfirmDialogMotion() {
const [open, setOpen] = useState(false);
const replay = () => {
setOpen(false);
setTimeout(() => setOpen(true), 400);
};
return (
<>
<Button variant="ghost" onClick={replay}>
Replay
</Button>
<ConfirmDialog
open={open}
onOpenChange={setOpen}
title="Delete Orbital Freight?"
description="The company and its notes go for good."
action="Delete company"
onAction={() => setOpen(false)}
/>
</>
);
}Workspace and account menu
The Agency name at the top of the sidebar. It opens one menu: who you are signed in as, the Agencies you can switch to, settings, bug reports and sign out.
Drawer (side sheet)
A panel from the right that shows one record while the list stays in view. Used for application peeks, the review sheet and meetings.