Alert dialog
An Apple-style alert on strong glass: a bold title, a short message and capsule actions — for confirming destructive or irreversible choices. Only its buttons close it.
Installation
pnpm dlx shadcn@latest add https://glasscn.app/r/alert-dialog.jsonReact Native (iOS, Android and web through react-native-web) — same parts and props:
pnpm dlx shadcn@latest add https://glasscn.app/r/native-alert-dialog.jsonOr with the namespace set up: shadcn add @glasscn-app/alert-dialog
Props
| Prop | Type | Default |
|---|---|---|
| size (AlertDialogContent) | "default" | "sm"Default: up to 28rem, buttons stacked on phones and in a trailing row from sm. sm: 20rem, two buttons side by side at equal width. | "default" |
| variant (AlertDialogAction) | "default" | "destructive" | …Button variantsThe default action: solid accent, or the tinted destructive button for an action the user didn't deliberately choose. | "default" |
| variant (AlertDialogCancel) | Button variantA soft fill — never glass on the glass panel. | "secondary" |
| open / defaultOpen / onOpenChange | Radix AlertDialogControlled or not, as Radix. | — |
Use AlertDialog for destructive or irreversible confirmations; Dialog for everything else (forms, details, anything the user may dismiss by clicking away).
role="alertdialog": an outside click doesn't close it, only its buttons and Escape (which cancels). Cancel is focused on open.
Parts (shadcn's API): AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogMedia, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, plus AlertDialogPortal and AlertDialogOverlay.
Write Cancel first, then the action: on phones the action stacks on top and Cancel at the bottom; from sm they sit in a trailing row with Cancel leading.
Apple's rules: a title of two lines at most, a short message, at most three buttons labelled with one- or two-word verbs, and always a Cancel. Don't use an alert for undoable deletes or plain information.
The page behind dims by --glass-dim with no blur; enter and exit run on --glass-duration and still under reduced motion.
Source
"use client"
import * as React from "react"
import { cn } from "cn"
import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
import { Button } from "@/components/glass/button"
/**
* shadcn's AlertDialog on glass, laid out like an Apple alert: a bold, start-aligned
* title, a short message and up to three capsule buttons — the default action
* trailing (row) or on top (stacked), Cancel leading or at the bottom. Only the
* buttons close it: no outside-click dismiss; Escape cancels.
*/
function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
}
function AlertDialogPortal({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
}
/** The page dims behind the alert (--glass-dim). No blur: the panel already frosts. */
function AlertDialogOverlay({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
data-glass-motion=""
className={cn(
"fixed inset-0 isolate z-50 bg-glass-dim duration-(--glass-duration) ease-glass data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
/>
)
}
/** A liquid-glass panel (the control layer). Never taller than the viewport — a long message scrolls. */
function AlertDialogContent({
className,
size = "default",
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & { size?: "default" | "sm" }) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
data-size={size}
data-glass-motion=""
className={cn(
"group/alert-dialog-content glass-liquid fixed inset-0 z-50 m-auto grid h-fit max-h-[calc(100dvh-2rem)] w-full max-w-[calc(100%-2rem)] gap-4 overflow-y-auto overscroll-contain rounded-surface p-(--alert-spacing) text-sm text-popover-foreground outline-hidden glass-materialize [--alert-spacing:calc(1.5rem*var(--glass-density))] data-[size=default]:sm:max-w-md data-[size=sm]:max-w-xs data-[size=sm]:[--alert-spacing:calc(1.25rem*var(--glass-density))]",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
/** Media, title and message, start-aligned (RTL-safe). */
function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="alert-dialog-header" className={cn("grid justify-items-start gap-1.5 text-start", className)} {...props} />
}
/**
* The buttons — write Cancel first, then the action. Stacked on phones (reversed,
* so the action is on top and Cancel at the bottom); from `sm` a trailing row with
* Cancel leading. `size="sm"` puts two buttons side by side at equal width.
*/
function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 pt-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
/** An icon tile above the title, on a fill (never more glass inside the panel). */
function AlertDialogMedia({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-media"
className={cn("mb-1.5 inline-flex size-control-lg items-center justify-center rounded-control bg-fill text-foreground *:[svg:not([class*='size-'])]:size-6", className)}
{...props}
/>
)
}
/** Bold, start-aligned; keep it to two lines. */
function AlertDialogTitle({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return <AlertDialogPrimitive.Title data-slot="alert-dialog-title" className={cn("type-glass-heading text-lg text-balance", className)} {...props} />
}
function AlertDialogDescription({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-sm text-pretty text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground", className)}
{...props}
/>
)
}
/** The default action. `variant="destructive"` for a destructive one (tinted, not solid). */
function AlertDialogAction({
className,
variant = "default",
size = "default",
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> & Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
return (
<Button variant={variant} size={size} asChild>
<AlertDialogPrimitive.Action data-slot="alert-dialog-action" className={cn(className)} {...props} />
</Button>
)
}
/** Cancel: a fill button (`secondary`) — never glass on the glass panel. Focused on open. */
function AlertDialogCancel({
className,
variant = "secondary",
size = "default",
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> & Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
return (
<Button variant={variant} size={size} asChild>
<AlertDialogPrimitive.Cancel data-slot="alert-dialog-cancel" className={cn(className)} {...props} />
</Button>
)
}
export {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogOverlay,
AlertDialogPortal,
AlertDialogTitle,
AlertDialogTrigger,
}