Alert Dialog
A blocking confirmation dialog. Unlike Dialog, it cannot be dismissed by clicking the backdrop or pressing Escape — the user must explicitly choose an action.
Key difference from Dialog: No backdrop click dismiss, no Escape key dismiss, no × close button. Use this only for actions that require deliberate acknowledgement.
Theme Preview
Destructive variant — forced light and dark styling for direct comparison.
Delete project?
This will permanently delete your project and all associated data.
Delete project?
This will permanently delete your project and all associated data.
Live Demo
Click the button to open the alert dialog. Notice the backdrop click and Escape key do not dismiss it.
Code Snippet
import { AlertDialog } from "@/ui/AlertDialog"; const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const handleConfirm = async () => { setLoading(true); await deleteItem(); setLoading(false); setOpen(false); }; <Button variant="destructive" onClick={() => setOpen(true)}> Delete project </Button> <AlertDialog.Root open={open} onClose={() => setOpen(false)} onConfirm={handleConfirm} title="Delete project?" description="This will permanently delete your project and all associated data. This action cannot be undone." confirmLabel="Delete project" cancelLabel="Keep it" variant="destructive" isLoading={loading} /> // Variants: "destructive" | "warning" | "default"
Bentuk lama <AlertDialog /> tetap jalan. Bentuk baru namespaced yang direkomendasikan adalah <AlertDialog.Root />.
Copy-Paste (Single File)
Versi ringkas self-contained yang bisa dipindahkan langsung ke project lain.
"use client"; import { useEffect, type ReactNode } from "react"; import { createPortal } from "react-dom"; type ClassValue = string | false | null | undefined; const cn = (...classes: ClassValue[]) => classes.filter(Boolean).join(" "); type AlertDialogVariant = "destructive" | "warning" | "default"; interface AlertDialogProps { open: boolean; onClose: () => void; onConfirm: () => void; title: string; description: string; cancelLabel?: string; confirmLabel?: string; variant?: AlertDialogVariant; isLoading?: boolean; } const CONFIRM_CLASSES: Record<AlertDialogVariant, string> = { destructive: "bg-red-500 text-white hover:bg-red-600", warning: "bg-amber-500 text-white hover:bg-amber-600", default: "bg-blue-600 text-white hover:bg-blue-700", }; function Spinner() { return ( <svg className="h-4 w-4 animate-spin" viewBox="0 0 16 16" fill="none"> <circle cx="8" cy="8" r="6" stroke="currentColor" strokeOpacity="0.25" strokeWidth="2" /> <path d="M14 8a6 6 0 0 0-6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /> </svg> ); } function AlertDialogRoot({ open, onClose, onConfirm, title, description, cancelLabel = "Cancel", confirmLabel = "Confirm", variant = "default", isLoading = false, }: AlertDialogProps) { useEffect(() => { if (!open) return; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = ""; }; }, [open]); if (!open) return null; return createPortal( <div className="fixed inset-0 z-50 flex items-center justify-center p-4"> <div className="absolute inset-0 bg-black/50" /> <div role="alertdialog" aria-modal="true" className="relative w-full max-w-md rounded-2xl border border-slate-200 bg-white shadow-2xl shadow-black/20"> <div className="p-6"> <h2 className="text-base font-semibold text-slate-900">{title}</h2> <p className="mt-1.5 text-sm leading-relaxed text-slate-500">{description}</p> </div> <div className="flex items-center justify-end gap-3 px-6 pb-6"> <button onClick={onClose} disabled={isLoading} className="rounded-lg border border-slate-200 px-4 py-2 text-sm font-medium text-slate-600 disabled:opacity-50"> {cancelLabel} </button> <button onClick={onConfirm} disabled={isLoading} className={cn("inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium disabled:opacity-70", CONFIRM_CLASSES[variant])}> {isLoading && <Spinner />} {confirmLabel} </button> </div> </div> </div>, document.body ); } type AlertDialogCompound = typeof AlertDialogRoot & { Root: typeof AlertDialogRoot }; export const AlertDialog = Object.assign(AlertDialogRoot, { Root: AlertDialogRoot }) as AlertDialogCompound; export type AlertDialogTriggerProps = { onClick: () => void; children: ReactNode; };
Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controls visibility. |
onClose | () => void | — | Called when Cancel is clicked. Backdrop and Escape do NOT trigger this. |
onConfirm | () => void | — | Called when the confirm button is clicked. |
title | string | — | Bold heading inside the dialog. |
description | string | — | Muted supporting text below the title. |
cancelLabel | string | "Cancel" | Label for the cancel button. |
confirmLabel | string | "Confirm" | Label for the confirm button. |
variant | "destructive" | "warning" | "default" | "default" | Controls icon, icon background, and confirm button colour. |
isLoading | boolean | false | Shows a spinner and disables both buttons during async operations. |