Componentfeedback
ConfirmDialog
Self-contained confirm dialog with focus trap and stamp entrance for confirm button in destructive mode.
Preview
Live Preview
Delete item
Are you sure you want to delete this item?
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add confirm-dialogOr install the full npm package:
npm install bigbulluiUsage
import { ConfirmDialog } from "@/components/ui/confirm-dialog"
<ConfirmDialog open={true} onOpenChange={() => {}} title="Delete item" description="Are you sure you want to delete this item?" confirmLabel="Delete" destructive={true} onConfirm={() => {}} onCancel={() => {}} />Source code
confirm-dialog.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
const FOCUSABLE = 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])';
export type ConfirmDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description?: string;
confirmLabel?: string;
destructive?: boolean;
onConfirm: () => void;
onCancel: () => void;
};
export function ConfirmDialog({
open,
onOpenChange,
title,
description,
confirmLabel = "OK",
destructive = false,
onConfirm,
onCancel,
}: ConfirmDialogProps) {
const dialogRef = React.useRef<HTMLDivElement>(null);
const restoreRef = React.useRef<HTMLElement | null>(null);
React.useEffect(() => {
if (!open) return;
restoreRef.current = document.activeElement as HTMLElement | null;
const content = dialogRef.current;
const focusables = content?.querySelectorAll<HTMLElement>(FOCUSABLE);
(focusables?.[0] ?? content)?.focus();
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
event.stopPropagation();
onCancel();
}
};
document.addEventListener("keydown", onKeyDown);
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKeyDown);
document.body.style.overflow = "";
if (restoreRef.current) {
restoreRef.current.focus();
}
};
}, [open, onOpenChange]);
if (!open) return null;
return (
<div
ref={dialogRef}
className={cn(
"fixed inset-0 z-50 items-center justify-center bg-black/50 backdrop-blur-sm",
"motion-reduce:animate-none",
)}
>
<div
ref={dialogRef}
className={cn(
"bg-card rounded-md border border-border w-80 max-w-full shadow-lg",
"motion-reduce:animate-none",
)}
>
<div className={cn("p-6", "motion-reduce:animate-none")}>
<h3 className={cn("text-lg font-semibold tracking-tight", "motion-reduce:animate-none")}>
{title}
</h3>
{description && (
<p className={cn("text-sm text-muted-foreground mt-2", "motion-reduce:animate-none")}>
{description}
</p>
)}
<div className={cn("mt-6 flex gap-3 justify-end", "motion-reduce:animate-none")}>
<button
onClick={onCancel}
className={cn(
"px-4 py-2 text-sm text-muted-foreground rounded-md hover:bg-muted hover:text-foreground transition-colors",
"motion-reduce:transition-none",
"motion-reduce:animate-none",
)}
>
Cancel
</button>
<button
onClick={onConfirm}
className={cn(
destructive
? "px-4 py-2 text-sm font-medium rounded-md bg-destructive text-destructive hover:bg-destructive/90"
: "px-4 py-2 text-sm font-medium rounded-md bg-accent text-accent-foreground hover:bg-accent/90",
"motion-reduce:transition-none",
"motion-reduce:animate-none",
)}
>
{confirmLabel}
</button>
</div>
</div>
</div>
</div>
);
}Props
| Prop | Type | Description |
|---|---|---|
| open | boolean | Controls the open state of the dialog. |
| onOpenChange | (open: boolean) => void | Callback when open state changes. |
| title | string | Dialog title. |
| description | string | Optional description text. |
| confirmLabel | string | Label for the confirm button, defaults to "OK". |
| destructive | boolean | Shows destructive styling (red) on confirm button when true. |
| onConfirm | () => void | Callback when confirm is clicked. |
| onCancel | () => void | Callback when cancel is clicked. |