AlertDialog

A modal dialog that interrupts the user with important content and expects a response.

Demo

Dependencies

clsxtailwind-merge

Props

This is a base primitive. It accepts standard HTML attributes plus className for styling via cn().

Source

"use client";

import {
  useCallback,
  useEffect,
  useId,
  useRef,
  createContext,
  useContext,
} from "react";
import { cn } from "@/lib/utils";

interface AlertDialogContextValue {
  open: boolean;
  onOpenChange: (open: boolean) => void;
}
const AlertDialogCtx = createContext<AlertDialogContextValue>({
  open: false,
  onOpenChange: () => {},
});

export function AlertDialog({
  open = false,
  onOpenChange,
  children,
}: {
  open?: boolean;
  onOpenChange?: (v: boolean) => void;
  children?: React.ReactNode;
}) {
  return (
    <AlertDialogCtx.Provider
      value={{ open, onOpenChange: onOpenChange ?? (() => {}) }}
    >
      {children}
    </AlertDialogCtx.Provider>
  );
}

export function AlertDialogTrigger({
  children,
  className,
}: {
  children?: React.ReactNode;
  className?: string;
}) {
  const { onOpenChange } = useContext(AlertDialogCtx);
  return (
    <button onClick={() => onOpenChange(true)} className={className}>
      {children}
    </button>
  );
}

export function AlertDialogContent({
  children,
  className,
}: {
  children?: React.ReactNode;
  className?: string;
}) {
  const { open, onOpenChange } = useContext(AlertDialogCtx);
  const overlayRef = useRef<HTMLDivElement>(null);
  const id = useId();

  useEffect(() => {
    const h = (e: KeyboardEvent) => {
      if (e.key === "Escape") onOpenChange(false);
    };
    if (open) {
      document.addEventListener("keydown", h);
      document.body.style.overflow = "hidden";
    }
    return () => {
      document.removeEventListener("keydown", h);
      document.body.style.overflow = "";
    };
  }, [open, onOpenChange]);

  if (!open) return null;

  return (
    <div
      ref={overlayRef}
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
      onClick={(e) => {
        if (e.target === overlayRef.current) onOpenChange(false);
      }}
    >
      <div
        role="alertdialog"
        aria-modal="true"
        aria-labelledby={`${id}-title`}
        className={cn(
          "w-full max-w-md rounded-lg border border-gray-200 bg-white p-6 shadow-sm",
          className,
        )}
      >
        {children}
      </div>
    </div>
  );
}

export function AlertDialogHeader({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return <div className={cn("mb-4 space-y-1.5", className)} {...props} />;
}

export function AlertDialogTitle({
  className,
  ...props
}: React.HTMLAttributes<HTMLHeadingElement>) {
  return (
    <h2
      className={cn("text-lg font-semibold text-gray-900", className)}
      {...props}
    />
  );
}

export function AlertDialogDescription({
  className,
  ...props
}: React.HTMLAttributes<HTMLParagraphElement>) {
  return <p className={cn("text-sm text-gray-500", className)} {...props} />;
}

export function AlertDialogFooter({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div className={cn("mt-6 flex justify-end gap-2", className)} {...props} />
  );
}

export function AlertDialogCancel({
  children,
  className,
  ...props
}: React.ButtonHTMLAttributes<HTMLButtonElement>) {
  const { onOpenChange } = useContext(AlertDialogCtx);
  return (
    <button
      onClick={() => onOpenChange(false)}
      className={cn(
        "rounded-md border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50",
        className,
      )}
      {...props}
    >
      {children ?? "Cancel"}
    </button>
  );
}

export function AlertDialogAction({
  children,
  className,
  ...props
}: React.ButtonHTMLAttributes<HTMLButtonElement>) {
  const { onOpenChange } = useContext(AlertDialogCtx);
  return (
    <button
      onClick={() => onOpenChange(false)}
      className={cn(
        "rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800",
        className,
      )}
      {...props}
    >
      {children ?? "Continue"}
    </button>
  );
}

CLI

npx genui add alert-dialog