Alert Dialog
pnpm dlx shadcn@latest add …/r/alert-dialog.jsonAn accessible React alert dialog for Next.js that asks people to confirm destructive actions, with focus starting on Cancel, built on Radix Alert Dialog.
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
export function DeleteAccount() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">Delete account</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete your account?</AlertDialogTitle>
<AlertDialogDescription>
Your projects and settings go with it. You can't undo this.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={deleteAccount}>
Delete account
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
export default function AlertDialogDeleteProject() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">Delete project</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete project?</AlertDialogTitle>
<AlertDialogDescription>
This permanently deletes Acme Web, with its deployments and domains. You can't undo this.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="destructive">Delete project</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}Installation
Install the dependencies.
$ pnpm add class-variance-authority motion radix-uiAdd the FewTone UI tokens to your global CSS file. Skip any variable your project already defines.
:root { --size-control-sm: 2rem; --size-control-md: 2.25rem; --size-control-lg: 2.5rem; --size-icon-sm: 0.875rem; --size-icon-md: 1rem; --size-icon-lg: 1.25rem; --size-roll-fade: 0.25em; --size-roll-fade-x: 0.5em; --duration-instant: 80ms; --duration-fast: 150ms; --duration-base: 220ms; --duration-slow: 350ms; --duration-slower: 500ms; --z-overlay: 1200; --z-modal: 1300; --z-popover: 1400; --z-toast: 1500; --z-tooltip: 1600; --focus-ring-width: 2px; --focus-ring-offset: 2px; --opacity-disabled: 0.5; --ease-spring: linear(0, 0.157, 0.48, 0.77, 0.97, 1.07, 1.1, 1.09, 1.05, 1.02, 1, 0.995, 1); --destructive-foreground: oklch(1 0 0); --overlay: color-mix(in oklch, oklch(0 0 0) 40%, transparent); } .dark { --destructive-foreground: oklch(1 0 0); --overlay: color-mix(in oklch, oklch(0 0 0) 60%, transparent); } @theme inline { --animate-popover-in: popover-in var(--duration-fast) cubic-bezier(0.22, 1, 0.36, 1) both; --animate-popover-out: popover-out var(--duration-instant) cubic-bezier(0.55, 0, 1, 0.45) both; --animate-collapsible-open: collapsible-open var(--duration-slow) cubic-bezier(0.22, 1, 0.36, 1); --animate-collapsible-close: collapsible-close var(--duration-base) cubic-bezier(0.55, 0, 1, 0.45) both; --color-destructive-foreground: var(--destructive-foreground); --color-overlay: var(--overlay); @keyframes popover-in { from { opacity: 0; filter: blur(6px); transform: scale(0.96); } to { opacity: 1; filter: blur(0); transform: scale(1); } } @keyframes popover-out { from { opacity: 1; filter: blur(0); transform: scale(1); } to { opacity: 0; filter: blur(6px); transform: scale(0.96); } } @keyframes collapsible-open { from { height: 0; opacity: 0; filter: blur(6px); } to { height: var(--collapsible-height); opacity: 1; filter: blur(0); } } @keyframes collapsible-close { from { height: var(--collapsible-height); opacity: 1; filter: blur(0); } to { height: 0; opacity: 0; filter: blur(6px); } } } @media (prefers-reduced-motion: reduce) { :root { --duration-instant: 1ms; --duration-fast: 1ms; --duration-base: 1ms; --duration-slow: 1ms; --duration-slower: 1ms; } }Copy the source into your project.
"use client"; import { buttonVariants } from "@/components/ui/button"; import { type DialogState, dialogOverlayMotion, dialogStyles, keepMovedFocus, useDialogState, useDialogWindow, useMergedRef, } from "@/components/ui/dialog"; import { cn } from "@/lib/utils"; import type { VariantProps } from "class-variance-authority"; import { AnimatePresence, motion } from "motion/react"; import { AlertDialog as AlertDialogPrimitive } from "radix-ui"; import { type ComponentProps, createContext, useContext } from "react"; /* * Alert Dialog, built on Radix Alert Dialog: a modal that asks people to confirm something, usually an action * they can't undo. It's announced as an alert dialog, focus starts on Cancel (the safe choice), Escape * cancels, and clicking the backdrop does nothing, so it's never dismissed by accident. Focus goes back to * the trigger. * - The same window, motion and open state as Dialog, shared from its file: it grows out of its trigger * through the blur on spring.snappy, leaves on a timed duration.base + ease.in, and the backdrop only * fades. Controlled (`open`) or not (`defaultOpen`). While it leaves, clicks go through to the page. * - Narrower than Dialog (max-w-md): it holds a question and two answers. * - Action and Cancel are buttons (buttonVariants). Use `variant="destructive"` on Action for deleting. */ const AlertDialogContext = createContext<DialogState | null>(null); function useAlertDialog(part: string) { const dialog = useContext(AlertDialogContext); if (!dialog) throw new Error(`${part} must be used inside AlertDialog`); return dialog; } function AlertDialog({ open, defaultOpen, onOpenChange, ...props }: ComponentProps<typeof AlertDialogPrimitive.Root>) { const { state, setOpen } = useDialogState({ open, defaultOpen, onOpenChange }); return ( <AlertDialogContext.Provider value={state}> <AlertDialogPrimitive.Root data-slot="alert-dialog" open={state.open} onOpenChange={setOpen} {...props} /> </AlertDialogContext.Provider> ); } function AlertDialogTrigger({ ref, ...props }: ComponentProps<typeof AlertDialogPrimitive.Trigger>) { const dialog = useAlertDialog("AlertDialogTrigger"); const setRef = useMergedRef(dialog.trigger, ref); return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" ref={setRef} {...props} />; } /** Stays mounted while the dialog is open and while its exit plays */ function AlertDialogPortal({ children, ...props }: Omit<ComponentProps<typeof AlertDialogPrimitive.Portal>, "forceMount">) { const { open } = useAlertDialog("AlertDialogPortal"); return ( <AnimatePresence initial={false}> {open ? ( <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} forceMount> {children} </AlertDialogPrimitive.Portal> ) : null} </AnimatePresence> ); } function AlertDialogOverlay({ className, style, ...props }: ComponentProps<typeof AlertDialogPrimitive.Overlay>) { const { open } = useAlertDialog("AlertDialogOverlay"); return ( <AlertDialogPrimitive.Overlay data-slot="alert-dialog-overlay" className={cn(dialogStyles.overlay, className)} // While it fades out, clicks go through to the page style={{ pointerEvents: open ? "auto" : "none", ...style }} {...props} asChild forceMount > <motion.div {...dialogOverlayMotion} /> </AlertDialogPrimitive.Overlay> ); } function AlertDialogContent({ className, children, onCloseAutoFocus, ...props }: ComponentProps<typeof AlertDialogPrimitive.Content>) { const dialog = useAlertDialog("AlertDialogContent"); const windowMotion = useDialogWindow(dialog.trigger); return ( <AlertDialogPortal> <AlertDialogOverlay /> <div data-slot="alert-dialog-positioner" className={dialogStyles.positioner}> <AlertDialogPrimitive.Content data-slot="alert-dialog-content" className={cn(dialogStyles.window, "max-w-md", className)} onCloseAutoFocus={(event) => { onCloseAutoFocus?.(event); keepMovedFocus(event); }} {...props} asChild forceMount > <motion.div {...windowMotion}>{children}</motion.div> </AlertDialogPrimitive.Content> </div> </AlertDialogPortal> ); } function AlertDialogHeader({ className, ...props }: ComponentProps<"div">) { return <div data-slot="alert-dialog-header" className={cn(dialogStyles.header, className)} {...props} />; } /** Cancel, then the action: stacked on phones with the action on top, in a row at the end from sm */ function AlertDialogFooter({ className, ...props }: ComponentProps<"div">) { return <div data-slot="alert-dialog-footer" className={cn(dialogStyles.footer, className)} {...props} />; } function AlertDialogTitle({ className, ...props }: ComponentProps<typeof AlertDialogPrimitive.Title>) { return ( <AlertDialogPrimitive.Title data-slot="alert-dialog-title" className={cn(dialogStyles.title, className)} {...props} /> ); } function AlertDialogDescription({ className, ...props }: ComponentProps<typeof AlertDialogPrimitive.Description>) { return ( <AlertDialogPrimitive.Description data-slot="alert-dialog-description" className={cn(dialogStyles.description, className)} {...props} /> ); } type ButtonStyle = VariantProps<typeof buttonVariants>; /** Confirms and closes. A Button in every way: `variant="destructive"` for deleting */ function AlertDialogAction({ className, variant, size, ...props }: ComponentProps<typeof AlertDialogPrimitive.Action> & ButtonStyle) { return ( <AlertDialogPrimitive.Action data-slot="alert-dialog-action" className={cn(buttonVariants({ variant, size }), className)} {...props} /> ); } /** Closes without doing anything; it has focus when the dialog opens */ function AlertDialogCancel({ className, variant = "outline", size, ...props }: ComponentProps<typeof AlertDialogPrimitive.Cancel> & ButtonStyle) { return ( <AlertDialogPrimitive.Cancel data-slot="alert-dialog-cancel" className={cn(buttonVariants({ variant, size }), className)} {...props} /> ); } export { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, };Update the import paths to match your project.
API reference
AlertDialog
The root. It keeps the open state, so the window can play its exit before it unmounts.
| Prop | Type | Default | Description |
|---|---|---|---|
open? | boolean | – | Whether it's open, when you control it. |
defaultOpen? | boolean | false | Whether it starts open. |
onOpenChange? | (open: boolean) => void | – | Called when it opens or closes: the trigger, Cancel, the action or Escape. |
AlertDialogTrigger
The button that opens it. Focus comes back here when it closes.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild? | boolean | false | Use your own element, such as a Button, as the trigger. |
AlertDialogContent
The window, with its backdrop, in a portal: the same look and motion as Dialog, a little narrower, without a close button. Announced as an alertdialog, named by its title and described by its description. Also accepts every Radix Alert Dialog Content prop.
| Prop | Type | Default | Description |
|---|---|---|---|
onOpenAutoFocus? | (event: Event) => void | – | Runs as focus moves to Cancel. Call event.preventDefault() to focus something else yourself. |
onCloseAutoFocus? | (event: Event) => void | – | Runs as focus goes back to the trigger. Call event.preventDefault() to send it elsewhere. |
onEscapeKeyDown? | (event: KeyboardEvent) => void | – | Runs when Escape is pressed. Call event.preventDefault() to keep it open. |
AlertDialogHeader
Holds the title and description.
| Prop | Type | Default | Description |
|---|
AlertDialogFooter
Holds Cancel and the action: stacked on phones with the action on top, in a row at the end from the sm breakpoint.
| Prop | Type | Default | Description |
|---|
AlertDialogTitle
The question, and the dialog's accessible name. Say what will happen: “Delete project?”.
| Prop | Type | Default | Description |
|---|
AlertDialogDescription
What the action does and whether it can be undone, read out after the name.
| Prop | Type | Default | Description |
|---|
AlertDialogAction
Confirms and closes. It's a Button, so it takes Button's variant and size. Your onClick runs first; call event.preventDefault() there to keep the dialog open.
| Prop | Type | Default | Description |
|---|---|---|---|
variant? | "default" | "secondary" | "outline" | "ghost" | "destructive" | "link" | "default" | The button style. Use destructive for actions that remove things. |
size? | "sm" | "md" | "lg" | "md" | The button height. |
AlertDialogCancel
Closes without doing anything. It has focus when the dialog opens.
| Prop | Type | Default | Description |
|---|---|---|---|
variant? | "default" | "secondary" | "outline" | "ghost" | "destructive" | "link" | "outline" | The button style. |
size? | "sm" | "md" | "lg" | "md" | The button height. |
AlertDialogPortal
Renders its children into document.body while the dialog is open, and while it leaves. AlertDialogContent already uses it.
| Prop | Type | Default | Description |
|---|---|---|---|
container? | HTMLElement | document.body | Where to render instead. |
AlertDialogOverlay
The backdrop that dims the page. It only fades, and clicking it does nothing. AlertDialogContent already includes it.
| Prop | Type | Default | Description |
|---|
Related components
An accessible React dialog for Next.js and Tailwind CSS: a modal window that grows out of its trigger, keeps focus inside and returns it, built on Radix Dialog.
An accessible React button for Next.js and Tailwind CSS with six variants, three sizes, icons and a soft tint on hover and press, built on a native button.
- Right-click here
An accessible React context menu for Next.js: a right-click menu with a gliding highlight, shortcuts, checkbox and radio items and sub-menus, built on Radix.