bigbullui logobigbullui
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-dialog

Or install the full npm package:

npm install bigbullui

Usage

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.tsx

Zero 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

PropTypeDescription
openbooleanControls the open state of the dialog.
onOpenChange(open: boolean) => voidCallback when open state changes.
titlestringDialog title.
descriptionstringOptional description text.
confirmLabelstringLabel for the confirm button, defaults to "OK".
destructivebooleanShows destructive styling (red) on confirm button when true.
onConfirm() => voidCallback when confirm is clicked.
onCancel() => voidCallback when cancel is clicked.