Skip to content

Confirmation dialog

'use client';
import { Button, Dialog, Stack, Text } from '@helixui/core';
export interface ConfirmDialogProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description?: string;
confirmLabel?: string;
cancelLabel?: string;
/** When true, the confirm button uses the danger tone. */
destructive?: boolean;
onConfirm: () => Promise<void> | void;
}
export function ConfirmDialog({
isOpen,
onOpenChange,
title,
description,
confirmLabel = 'Confirm',
cancelLabel = 'Cancel',
destructive = false,
onConfirm,
}: ConfirmDialogProps) {
const handleConfirm = async () => {
await onConfirm();
onOpenChange(false);
};
return (
<Dialog isOpen={isOpen} onOpenChange={onOpenChange} aria-labelledby="confirm-title">
<Stack gap={4} style={{ padding: 'var(--helixui-space-5)', maxWidth: 420 }}>
<Stack gap={1}>
<Text id="confirm-title" size="lg" weight="semibold">{title}</Text>
{description ? <Text size="sm" tone="muted">{description}</Text> : null}
</Stack>
<Stack direction="row" justify="end" gap={2}>
<Button variant="ghost" tone="neutral" onClick={() => onOpenChange(false)}>{cancelLabel}</Button>
<Button tone={destructive ? 'danger' : 'brand'} onClick={handleConfirm}>{confirmLabel}</Button>
</Stack>
</Stack>
</Dialog>
);
}

Usage

const [open, setOpen] = useState(false);
<>
<Button tone="danger" onClick={() => setOpen(true)}>Delete project</Button>
<ConfirmDialog
isOpen={open}
onOpenChange={setOpen}
title="Delete this project?"
description="This permanently removes the project, every file, and every link to it. This cannot be undone."
confirmLabel="Delete"
destructive
onConfirm={() => deleteProject(id)}
/>
</>

The dialog traps focus, restores focus to the trigger on close, and closes on Escape — those are Dialog defaults, not block-specific.