A confirm-destructive dialog: alertdialog semantics, cancel focused by default, outside clicks never dismiss, house modal physics.
npx shadcn@latest add @paragon/alert-dialogAlso installs: button
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { cn } from "@/lib/utils";
import { Button, type ButtonProps } from "@/registry/paragon/ui/button";
/*
* Confirm-destructive dialog built on Radix Dialog with alertdialog
* semantics: role="alertdialog", outside interactions never dismiss,
* and the cancel button receives initial focus so Enter can't destroy
* anything by default. Same modal physics as `dialog` — center-origin
* scale 0.97 -> 1 at 200ms, 150ms non-retracing exit.
*/
const alertDialogStyles = `
@keyframes pg-alert-overlay-in { from { opacity: 0; } }
@keyframes pg-alert-overlay-out { to { opacity: 0; } }
@keyframes pg-alert-in { from { opacity: 0; scale: 0.97; } }
@keyframes pg-alert-out { to { opacity: 0; scale: 0.99; } }
@media (prefers-reduced-motion: reduce) {
@keyframes pg-alert-in { from { opacity: 0; } }
@keyframes pg-alert-out { to { opacity: 0; } }
}
`;
interface AlertDialogContextValue {
cancelRef: React.RefObject<HTMLButtonElement | null>;
}
const AlertDialogContext = React.createContext<AlertDialogContextValue | null>(
null,
);
function AlertDialog(
props: React.ComponentProps<typeof DialogPrimitive.Root>,
) {
const cancelRef = React.useRef<HTMLButtonElement>(null);
const context = React.useMemo(() => ({ cancelRef }), []);
return (
<AlertDialogContext.Provider value={context}>
<DialogPrimitive.Root data-slot="alert-dialog" {...props} />
</AlertDialogContext.Provider>
);
}
function AlertDialogTrigger(
props: React.ComponentProps<typeof DialogPrimitive.Trigger>,
) {
return (
<DialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
);
}
function AlertDialogContent({
className,
onOpenAutoFocus,
onInteractOutside,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
const context = React.useContext(AlertDialogContext);
return (
<DialogPrimitive.Portal>
<style href="paragon-alert-dialog" precedence="paragon">{alertDialogStyles}</style>
<DialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/40 dark:bg-black/60",
"data-[state=open]:animate-[pg-alert-overlay-in_200ms_var(--ease-out)]",
"data-[state=closed]:animate-[pg-alert-overlay-out_150ms_var(--ease-exit)_forwards]",
)}
/>
<DialogPrimitive.Content
data-slot="alert-dialog-content"
role="alertdialog"
onOpenAutoFocus={(event) => {
onOpenAutoFocus?.(event);
if (event.defaultPrevented) return;
if (context?.cancelRef.current) {
event.preventDefault();
context.cancelRef.current.focus();
}
}}
onInteractOutside={(event) => {
onInteractOutside?.(event);
// Alert dialogs require an explicit choice — clicks outside never dismiss.
event.preventDefault();
}}
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-card p-6 text-card-foreground shadow-overlay outline-none sm:max-w-md",
"data-[state=open]:animate-[pg-alert-in_200ms_var(--ease-out)]",
"data-[state=closed]:animate-[pg-alert-out_150ms_var(--ease-exit)_forwards]",
className,
)}
{...props}
/>
</DialogPrimitive.Portal>
);
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-1.5", className)}
{...props}
/>
);
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-base leading-none font-semibold", className)}
{...props}
/>
);
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
}
/** The destructive confirm button. Closes the dialog on click. */
function AlertDialogAction({
variant = "destructive",
...props
}: ButtonProps) {
return (
<DialogPrimitive.Close asChild>
<Button data-slot="alert-dialog-action" variant={variant} {...props} />
</DialogPrimitive.Close>
);
}
/** The safe way out. Receives focus when the dialog opens. */
function AlertDialogCancel({ variant = "outline", ...props }: ButtonProps) {
const context = React.useContext(AlertDialogContext);
return (
<DialogPrimitive.Close asChild>
<Button
data-slot="alert-dialog-cancel"
ref={context?.cancelRef}
variant={variant}
{...props}
/>
</DialogPrimitive.Close>
);
}
export {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};