Dialog
The lights go down and a panel rises onto the desk, screwed or stitched at its corners. Content has no padding of its own, so it hosts forms and command palettes alike; AlertDialog asks before something can't be undone.
Delete a project
An alert dialog with a red key. Cancel takes focus, and a stray click outside does nothing.
acme-web · 14 deployments
A form
Rename a document. Enter saves; Escape, × or Cancel leave it.
Quarterly report
Command palette
⌘K opens a palette in the dialog: top-aligned, no padding, no fasteners.
Press ⌘K or Ctrl K.
Installation
Add with the CLI
Once per project, add the Bevel registry to
components.json.components.json { "registries": { "@bevel": "https://bevel-ui.vercel.app/r/{name}.json" } }Add Dialog. This brings in the Bevel foundation, installs @base-ui/react and cn, and merges the styles into your global CSS.
Terminal npx shadcn@latest add @bevel/dialog
Install manually
Install the dependencies.
Terminal pnpm add @base-ui/react cnAdd the Bevel foundation, if you haven't already. Light, depth, materials, motion and shape tokens, plus the utilities components use to apply them.
styles/bevel.css /* * Bevel foundation. Every component builds on these tokens and utilities; * component stylesheets only add their own palettes and shapes. * * Layers: * 1. Light one source, above and slightly in front of the screen. * 2. Depth named elevations built from the light: recessed, flush, * raised, pressed, object. * 3. Material a surface recipe (leather, plastic, metal) that reads * --bevel-material and --bevel-material-edge. * 4. Motion two curves, a spring, and durations named by purpose. * 5. Shape radii, stitching, and the type scale for amounts. */ :root { /* 1. Light */ --bevel-highlight: oklch(1 0 0 / 0.22); --bevel-highlight-strong: oklch(1 0 0 / 0.35); --bevel-shade: oklch(0 0 0 / 0.32); --bevel-shade-strong: oklch(0 0 0 / 0.5); --bevel-cast: oklch(0.25 0.03 50 / 0.22); /* Stamped into a surface: shaded on the upper inner edge, lit below. */ --bevel-deboss: 0 -1px 0 oklch(0 0 0 / 0.45), 0 1px 0 oklch(1 0 0 / 0.12); /* 3. Material defaults; components and variants override these. */ --bevel-material: oklch(0.47 0.085 52); --bevel-material-edge: oklch(0.3 0.06 44); --bevel-material-recess: oklch(0.2 0.008 260); /* neutral dark; materials set their own */ --bevel-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.7' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .7 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E"); --bevel-grain-size: 180px; /* 2. Depth */ --bevel-depth-recessed: inset 0 1px 2px var(--bevel-shade-strong), 0 1px 0 var(--bevel-highlight); --bevel-depth-flush: inset 0 1px 0 var(--bevel-highlight), inset 0 -1px 0 var(--bevel-shade); --bevel-depth-raised: inset 0 1px 0 var(--bevel-highlight), 0 1px 0 var(--bevel-material-edge), 0 2px 0 var(--bevel-material-edge), 0 3px 5px var(--bevel-shade); --bevel-depth-pressed: inset 0 1px 2px var(--bevel-shade), 0 1px 0 var(--bevel-highlight); /* A whole object resting on the page. */ --bevel-depth-object: inset 0 1px 0 var(--bevel-highlight), inset 0 -1px 0 var(--bevel-shade), 0 0 0 1px var(--bevel-material-edge), 0 1px 1px var(--bevel-cast), 0 6px 12px -2px var(--bevel-cast), 0 18px 32px -8px var(--bevel-cast); /* The shadow under a loose object, for filter: drop-shadow(). */ --bevel-depth-cast-sm: 0 4px 6px var(--bevel-cast); /* A thin object (a card) lying on another surface. */ --bevel-depth-sheet: inset 0 1px 0 var(--bevel-highlight-strong), inset 0 0 0 1px oklch(0 0 0 / 0.12), 0 -1px 2px oklch(0 0 0 / 0.18); /* 4. Motion */ --bevel-ease-out: cubic-bezier(0.23, 1, 0.32, 1); --bevel-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); /* Underdamped spring, ~8% overshoot. Pure CSS; pair with --bevel-duration-move or longer. */ --bevel-ease-spring: linear(0, 0.06, 0.202, 0.38, 0.561, 0.723, 0.856, 0.955, 1.023, 1.062, 1.081, 1.083, 1.075, 1.062, 1.047, 1.032, 1.019, 1.009, 1.002, 0.997, 0.994, 0.993, 0.993, 0.994, 1); --bevel-duration-press: 100ms; /* a control giving under a finger */ --bevel-duration-small: 200ms; /* hover lifts, tabs, small state changes */ --bevel-duration-move: 300ms; /* objects moving: cards, drawers */ --bevel-duration-ambient: 520ms; /* whole-scene changes: lighting */ --bevel-stagger: 30ms; --bevel-lift: 3px; /* how far an object rises on hover */ --bevel-perspective: 30rem; /* viewing distance for parts that turn in 3D (covers, flaps) */ /* Unchosen items in a group sit back in shadow. */ --bevel-dim: 0.82; /* 5. Shape */ --bevel-radius-xs: 0.25rem; --bevel-radius-sm: 0.5rem; --bevel-radius-md: 0.625rem; --bevel-radius-lg: 1rem; --bevel-radius-xl: 1.5rem; --bevel-stitch-width: 1.5px; --bevel-stitch-inset: 0.375rem; /* Amounts: the figure, its secondary parts (currency, cents), labels, fine print. */ --bevel-amount: 2.25rem; --bevel-amount-minor: 1.25rem; --bevel-label: 0.75rem; --bevel-fine: 0.6875rem; --bevel-tracking-label: 0.08em; /* stamped labels */ --bevel-tracking-print: 0.06em; /* printed marks on cards */ /* Paper and ink */ --bevel-paper: oklch(0.975 0.008 85); --bevel-paper-foreground: oklch(0.28 0.02 260); --bevel-paper-muted: oklch(0.48 0.015 260); --bevel-paper-rule: oklch(0.78 0.04 240 / 0.45); --bevel-paper-margin: oklch(0.68 0.14 25 / 0.45); --bevel-fibre: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='f'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9 .35' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .18 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23f)'/%3E%3C/svg%3E"); /* Ink soaks unevenly: mostly solid with pale specks. Used as a mask. */ --bevel-ink-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='i'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -2.4 0 0 0 2.1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23i)'/%3E%3C/svg%3E"); --bevel-ink-red: oklch(0.52 0.19 27); --bevel-ink-blue: oklch(0.44 0.14 262); --bevel-ink-green: oklch(0.5 0.12 155); --bevel-ink-black: oklch(0.3 0.01 260); --bevel-ink-amber: oklch(0.55 0.13 65); /* Brass and ivory: instrument hardware and dial faces */ --bevel-brass: oklch(0.76 0.11 82); --bevel-brass-edge: oklch(0.52 0.08 70); --bevel-ivory: oklch(0.95 0.025 90); --bevel-ivory-foreground: oklch(0.25 0.015 60); --bevel-needle: oklch(0.55 0.2 27); /* Desk stationery: legal pads, pens, pencils, exhibit stickers */ --bevel-pad: oklch(0.95 0.075 98); /* yellow legal pad */ --bevel-pad-foreground: oklch(0.27 0.03 255); --bevel-pad-rule: oklch(0.72 0.06 235 / 0.45); --bevel-pad-margin: oklch(0.62 0.16 25 / 0.55); --bevel-pen-red: oklch(0.52 0.2 27); /* correcting pen */ --bevel-pen-blue: oklch(0.42 0.15 262); /* writing pen */ --bevel-pencil: oklch(0.55 0.01 260); /* not written in ink yet */ --bevel-sticker: oklch(0.9 0.12 95); /* exhibit sticker */ --bevel-sticker-foreground: oklch(0.26 0.03 70); /* Handwriting. Bring a marker font (e.g. Permanent Marker) for the full effect; these are fallbacks. */ --bevel-font-marker: "Permanent Marker", "Marker Felt", "Chalkboard SE", ui-rounded, "Arial Rounded MT Bold", system-ui, sans-serif; } .dark { --bevel-highlight: oklch(1 0 0 / 0.14); /* Paper under dim light, not inverted: it's still paper. */ --bevel-paper: oklch(0.88 0.008 85); --bevel-ivory: oklch(0.86 0.022 90); --bevel-pad: oklch(0.86 0.07 98); --bevel-cast: oklch(0 0 0 / 0.55); } /* Depth */ @utility bevel-recessed { background-color: var(--bevel-material-recess); box-shadow: var(--bevel-depth-recessed); } @utility bevel-flush { box-shadow: var(--bevel-depth-flush); } @utility bevel-raised { background-color: color-mix(in oklch, var(--bevel-material), white 7%); background-image: linear-gradient(to bottom, var(--bevel-highlight), transparent 60%); box-shadow: var(--bevel-depth-raised); } @utility bevel-pressed { background-color: color-mix(in oklch, var(--bevel-material), black 4%); background-image: none; box-shadow: var(--bevel-depth-pressed); } @utility bevel-object { box-shadow: var(--bevel-depth-object); } /* Materials */ @utility bevel-leather { background-color: var(--bevel-material); background-image: var(--bevel-grain), linear-gradient(to bottom, var(--bevel-highlight), transparent 28%, transparent 72%, var(--bevel-shade)); background-size: var(--bevel-grain-size) var(--bevel-grain-size), 100% 100%; background-blend-mode: soft-light, normal; } @utility bevel-plastic { background-color: var(--bevel-material); background-image: linear-gradient(115deg, var(--bevel-highlight-strong), transparent 40%), linear-gradient(to bottom, transparent 25%, oklch(0 0 0 / 0.45)); box-shadow: var(--bevel-depth-sheet); } @utility bevel-metal { background-color: var(--bevel-material); background-image: linear-gradient(to bottom, var(--bevel-highlight-strong), transparent 45%, var(--bevel-shade) 100%), repeating-linear-gradient(90deg, oklch(1 0 0 / 0.06) 0 1px, transparent 1px 3px); box-shadow: inset 0 0 0 1px oklch(0 0 0 / 0.25), inset 0 1px 0 var(--bevel-highlight-strong); } /* Details */ @utility bevel-deboss { text-shadow: var(--bevel-deboss); } @utility bevel-stitch { border-color: var(--bevel-stitch, oklch(0.88 0.05 82 / 0.8)); border-style: dashed; border-width: var(--bevel-stitch-width); filter: drop-shadow(0 1px 0 var(--bevel-shade)); } /* A lit fill sitting in a recess (meters, progress). Colour from --bevel-fill. */ @utility bevel-fill { background-color: var(--bevel-fill); background-image: linear-gradient(to bottom, oklch(1 0 0 / 0.55), transparent 55%, oklch(0 0 0 / 0.18)); box-shadow: 0 0 6px -1px var(--bevel-fill); } /* A small indicator light. Colour from --bevel-led. */ @utility bevel-led { background-color: var(--bevel-led); background-image: radial-gradient(circle at 35% 30%, oklch(1 0 0 / 0.85), transparent 45%); box-shadow: 0 0 6px var(--bevel-led), inset 0 -1px 1px oklch(0 0 0 / 0.35), 0 1px 0 var(--bevel-highlight); } /* Paper: a sheet resting on the surface, with fibre in it. */ @utility bevel-paper { background-color: var(--bevel-paper); background-image: var(--bevel-fibre); background-size: 240px 240px; color: var(--bevel-paper-foreground); box-shadow: 0 1px 1px var(--bevel-cast), 0 6px 14px -4px var(--bevel-cast); } /* Ink on paper: multiplies into the surface and soaks unevenly. Colour from --bevel-ink. */ @utility bevel-ink { color: var(--bevel-ink, var(--bevel-ink-red)); border-color: currentColor; mix-blend-mode: multiply; mask-image: var(--bevel-ink-texture); mask-size: 160px 160px; } /* Glass over an instrument window: one reflection from the light above. Put on an overlay element. */ @utility bevel-glass { pointer-events: none; background-image: linear-gradient(165deg, var(--bevel-highlight-strong) 0%, transparent 38%, transparent 70%, var(--bevel-shade) 100%); } /* Material families: one per screen. Set on the screen's container. */ [data-bevel-family="leather"] { --bevel-cast: oklch(0.25 0.03 50 / 0.22); } [data-bevel-family="paper"] { --bevel-cast: oklch(0.3 0.01 250 / 0.16); } .dark :is([data-bevel-family="leather"], [data-bevel-family="paper"]) { --bevel-cast: oklch(0 0 0 / 0.55); } /* * Ruled paper: rules every --bevel-rule-height and a margin line at * --bevel-rule-margin. Set --bevel-sheet for the paper colour. The ruling * scrolls with the content. */ @utility bevel-ruled { background-color: var(--bevel-sheet, var(--bevel-paper)); background-image: linear-gradient( to right, transparent calc(var(--bevel-rule-margin) - 1px), var(--bevel-sheet-margin, var(--bevel-paper-margin)) calc(var(--bevel-rule-margin) - 1px) calc(var(--bevel-rule-margin) + 1px), transparent calc(var(--bevel-rule-margin) + 1px) ), repeating-linear-gradient( to bottom, transparent 0 calc(var(--bevel-rule-height) - 1px), var(--bevel-sheet-rule, var(--bevel-paper-rule)) calc(var(--bevel-rule-height) - 1px) var(--bevel-rule-height) ), var(--bevel-fibre); background-size: 100% 100%, 100% 100%, 240px 240px; background-attachment: local; color: var(--bevel-paper-foreground); box-shadow: 0 1px 1px var(--bevel-cast), 0 6px 14px -4px var(--bevel-cast); }Copy
dialog.tsxintocomponents/ui.components/dialog.tsx "use client" import * as React from "react" import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog" import { Dialog as DialogPrimitive } from "@base-ui/react/dialog" import { cn } from "cn" type Fasteners = "screws" | "stitch" | "none" /* The room's lights go down slowly, and come back up quicker. */ const dim = cn( "dialog-dim fixed inset-0 z-50", "transition-opacity duration-(--bevel-duration-ambient) ease-(--bevel-ease-in-out) motion-reduce:transition-none", "data-starting-style:opacity-0 data-ending-style:opacity-0 data-ending-style:duration-(--bevel-duration-move) data-ending-style:ease-(--bevel-ease-out)" ) const viewport = "fixed inset-0 z-50 flex items-center justify-center p-4 data-[align=top]:items-start data-[align=top]:pt-(--dialog-top)" /* The panel rises onto the desk and settles; it leaves quickly. */ const panel = cn( "dialog-panel relative flex max-h-full w-full flex-col overflow-hidden rounded-(--bevel-radius-lg) text-sm outline-none", "transition-[translate,opacity] duration-(--bevel-duration-move) ease-(--bevel-ease-spring) motion-reduce:transition-none", "data-starting-style:translate-y-(--dialog-rise) data-starting-style:opacity-0", "data-ending-style:translate-y-(--dialog-rise) data-ending-style:opacity-0 data-ending-style:duration-(--bevel-duration-small) data-ending-style:ease-(--bevel-ease-out)", "forced-colors:border forced-colors:border-[CanvasText]" ) const key = cn( "bevel-raised inline-flex h-8 items-center justify-center gap-1.5 rounded-(--bevel-radius-sm) px-3 text-sm font-medium whitespace-nowrap text-(--dialog-key-ink) outline-none select-none", "transition-[translate] duration-(--bevel-duration-press) ease-(--bevel-ease-out) motion-reduce:transition-none", "active:translate-y-px active:bevel-pressed focus-visible:ring-2 focus-visible:ring-(--bevel-pen-blue)/60 focus-visible:ring-offset-1", "data-disabled:pointer-events-none data-disabled:opacity-50", "forced-colors:border forced-colors:border-[ButtonText]" ) /** Screws at the four corners, each at its own angle, or a stitched edge. Decorative. */ function DialogFasteners({ fasteners }: { fasteners: Fasteners }) { if (fasteners === "none") return null if (fasteners === "stitch") return ( <span aria-hidden data-slot="dialog-stitch" className="pointer-events-none absolute inset-(--bevel-stitch-inset) rounded-[calc(var(--bevel-radius-lg)-var(--bevel-stitch-inset))] border-(length:--bevel-stitch-width) border-dashed border-(--dialog-stitch)" /> ) return ( <span aria-hidden data-slot="dialog-screws" className="pointer-events-none absolute inset-(--dialog-screw-inset)"> <span className="dialog-screw absolute top-0 left-0" /> <span className="dialog-screw absolute top-0 right-0" /> <span className="dialog-screw absolute bottom-0 left-0" /> <span className="dialog-screw absolute right-0 bottom-0" /> </span> ) } function CornerClose() { return ( <DialogPrimitive.Close data-slot="dialog-corner-close" aria-label="Close" className="absolute top-4 right-4 z-10 inline-flex size-7 items-center justify-center rounded-(--bevel-radius-sm) text-(--dialog-panel-muted) outline-none hover:text-(--dialog-panel-foreground) focus-visible:ring-2 focus-visible:ring-(--bevel-pen-blue)/60" > <svg aria-hidden viewBox="0 0 16 16" className="size-4" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round"> <path d="M4 4l8 8M12 4l-8 8" /> </svg> </DialogPrimitive.Close> ) } /* Dialog ------------------------------------------------------------------ */ /** A trigger and the panel it brings up. Escape, the backdrop and Close all put it away, and focus returns to the trigger. */ function Dialog(props: DialogPrimitive.Root.Props) { return <DialogPrimitive.Root data-slot="dialog" {...props} /> } /** Opens the dialog. Renders a button; pass `render` to use your own. */ function DialogTrigger(props: DialogPrimitive.Trigger.Props) { return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} /> } type DialogContentProps = DialogPrimitive.Popup.Props & { /** "top" sits the panel near the top of the screen, for a command palette. */ align?: "center" | "top" /** What holds the panel down at its corners. */ fasteners?: Fasteners /** A × in the top corner. Turn off when the content has its own way out. */ showCloseButton?: boolean } /** * The lights go down and the panel rises onto the desk. No padding of its * own: compose DialogHeader, DialogBody and DialogFooter, or put a command * palette straight in. */ function DialogContent({ className, children, align = "center", fasteners = "screws", showCloseButton = true, ...props }: DialogContentProps) { return ( <DialogPrimitive.Portal> <DialogPrimitive.Backdrop data-slot="dialog-backdrop" className={dim} /> <DialogPrimitive.Viewport data-slot="dialog-viewport" data-align={align} className={viewport}> <DialogPrimitive.Popup data-slot="dialog-content" data-fasteners={fasteners} className={cn(panel, "max-w-(--dialog-width)", className)} {...props} > <DialogFasteners fasteners={fasteners} /> {children} {showCloseButton && <CornerClose />} </DialogPrimitive.Popup> </DialogPrimitive.Viewport> </DialogPrimitive.Portal> ) } /** Title and description. Leaves room for the corner ×. */ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="dialog-header" className={cn("flex shrink-0 flex-col gap-1.5 px-6 pt-6 pr-12", className)} {...props} /> ) } /** Names the dialog for screen readers. */ function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) { return ( <DialogPrimitive.Title data-slot="dialog-title" className={cn("text-base leading-tight font-semibold", className)} {...props} /> ) } function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) { return ( <DialogPrimitive.Description data-slot="dialog-description" className={cn("text-sm text-(--dialog-panel-muted)", className)} {...props} /> ) } /** The content between header and footer. Scrolls when the panel runs out of room. */ function DialogBody({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="dialog-body" // Focusable, so a body that scrolls can be scrolled from the keyboard. tabIndex={0} className={cn( "min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 py-4 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", className )} {...props} /> ) } /** A ledge along the bottom, behind an engraved rule, holding the keys. */ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="dialog-footer" className={cn( "mt-4 flex shrink-0 flex-col-reverse gap-2 border-t border-t-(--dialog-rule-shade) bg-(--dialog-ledge) px-6 py-4 shadow-[inset_0_1px_0_var(--dialog-rule-light)] sm:flex-row sm:justify-end", "[[data-slot=dialog-body]+&]:mt-0", className )} {...props} /> ) } type DialogCloseProps = DialogPrimitive.Close.Props & { /** "destructive" makes it the red key. */ tone?: "default" | "destructive" } /** A key that closes the dialog. Use it for Cancel, Done, or an action that ends with closing. */ function DialogClose({ className, tone = "default", ...props }: DialogCloseProps) { return <DialogPrimitive.Close data-slot="dialog-close" data-tone={tone} className={cn(key, className)} {...props} /> } /* Alert dialog ------------------------------------------------------------ */ /** A dialog that needs an answer: it doesn't close on an outside click. For destructive confirmation. */ function AlertDialog(props: AlertDialogPrimitive.Root.Props) { return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} /> } function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) { return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} /> } type AlertDialogContentProps = AlertDialogPrimitive.Popup.Props & { fasteners?: Fasteners } /** The same panel, narrower, with no corner ×: answer it or press Escape. */ function AlertDialogContent({ className, children, fasteners = "screws", ...props }: AlertDialogContentProps) { return ( <AlertDialogPrimitive.Portal> <AlertDialogPrimitive.Backdrop data-slot="alert-dialog-backdrop" className={dim} /> <AlertDialogPrimitive.Viewport data-slot="alert-dialog-viewport" className={viewport}> <AlertDialogPrimitive.Popup data-slot="alert-dialog-content" data-fasteners={fasteners} className={cn(panel, "max-w-(--dialog-alert-width)", className)} {...props} > <DialogFasteners fasteners={fasteners} /> {children} </AlertDialogPrimitive.Popup> </AlertDialogPrimitive.Viewport> </AlertDialogPrimitive.Portal> ) } function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) { return <DialogHeader data-slot="alert-dialog-header" className={cn("pr-6", className)} {...props} /> } function AlertDialogTitle({ className, ...props }: AlertDialogPrimitive.Title.Props) { return ( <AlertDialogPrimitive.Title data-slot="alert-dialog-title" className={cn("text-base leading-tight font-semibold", className)} {...props} /> ) } function AlertDialogDescription({ className, ...props }: AlertDialogPrimitive.Description.Props) { return ( <AlertDialogPrimitive.Description data-slot="alert-dialog-description" className={cn("text-sm text-(--dialog-panel-muted)", className)} {...props} /> ) } function AlertDialogFooter(props: React.ComponentProps<"div">) { return <DialogFooter data-slot="alert-dialog-footer" {...props} /> } /** The safe answer. Put it first: it takes focus when the dialog opens. */ function AlertDialogCancel({ className, children = "Cancel", ...props }: AlertDialogPrimitive.Close.Props) { return ( <AlertDialogPrimitive.Close data-slot="alert-dialog-cancel" className={cn(key, className)} {...props}> {children} </AlertDialogPrimitive.Close> ) } type AlertDialogActionProps = AlertDialogPrimitive.Close.Props & { /** Red by default: alert dialogs are for things that can't be undone. */ tone?: "default" | "destructive" } /** Does the thing and closes. Runs onClick first, so do the work there. */ function AlertDialogAction({ className, tone = "destructive", ...props }: AlertDialogActionProps) { return ( <AlertDialogPrimitive.Close data-slot="alert-dialog-action" data-tone={tone} className={cn(key, className)} {...props} /> ) } export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter, DialogClose, AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, } export type { DialogContentProps, DialogCloseProps, AlertDialogContentProps, AlertDialogActionProps }Copy
dialog.cssintoyour styles.styles/dialog.css /* * Dialog: the room's lights go down and a panel rises onto the desk, * screwed (or stitched) at its corners. Builds on bevel.css (light, depth, * materials, motion, shape) and matches the theme change in * theme-transition.css: dimming is slow, lights coming up is quicker. */ :root { --dialog-panel: oklch(0.985 0.005 85); --dialog-panel-foreground: oklch(0.26 0.015 260); --dialog-panel-muted: oklch(0.5 0.015 260); --dialog-ledge: oklch(0.955 0.007 85); /* the footer strip, a step lower */ --dialog-rim: oklch(0 0 0 / 0.12); --dialog-rule-shade: oklch(0 0 0 / 0.1); --dialog-rule-light: oklch(1 0 0 / 0.8); /* The room with the lights down. */ --dialog-dim: oklch(0.14 0.01 260 / 0.42); --dialog-dim-brightness: 0.8; --dialog-screw: oklch(0.8 0.01 250); --dialog-screw-slot: oklch(0.35 0.01 250 / 0.7); --dialog-stitch: oklch(0.55 0.02 260 / 0.35); --dialog-key: oklch(0.97 0.004 85); --dialog-key-edge: oklch(0.76 0.008 250); --dialog-key-foreground: oklch(0.26 0.015 260); --dialog-danger: oklch(0.55 0.2 27); --dialog-danger-edge: oklch(0.38 0.15 27); --dialog-danger-foreground: oklch(0.99 0.005 90); --dialog-depth: inset 0 1px 0 var(--bevel-highlight-strong), 0 0 0 1px var(--dialog-rim), 0 2px 4px var(--bevel-cast), 0 30px 60px -20px var(--bevel-cast); --dialog-width: 32rem; --dialog-alert-width: 26rem; --dialog-top: 12vh; /* where a top-aligned dialog (a command palette) sits */ --dialog-rise: 1rem; /* how far the panel travels up onto the desk */ --dialog-screw-size: 0.4375rem; --dialog-screw-inset: 0.5rem; } .dark { --dialog-panel: oklch(0.29 0.008 260); --dialog-panel-foreground: oklch(0.95 0.005 90); --dialog-panel-muted: oklch(0.72 0.01 260); --dialog-ledge: oklch(0.255 0.008 260); --dialog-rim: oklch(1 0 0 / 0.08); --dialog-rule-shade: oklch(0 0 0 / 0.45); --dialog-rule-light: oklch(1 0 0 / 0.06); --dialog-dim: oklch(0.05 0.01 260 / 0.6); --dialog-dim-brightness: 0.6; --dialog-screw: oklch(0.55 0.01 250); --dialog-screw-slot: oklch(0.15 0.01 250 / 0.8); --dialog-stitch: oklch(0.85 0.02 260 / 0.25); --dialog-key: oklch(0.38 0.008 260); --dialog-key-edge: oklch(0.2 0.008 260); --dialog-key-foreground: oklch(0.95 0.005 90); --dialog-danger: oklch(0.55 0.19 27); --dialog-danger-edge: oklch(0.32 0.13 27); } @utility dialog-panel { background-color: var(--dialog-panel); background-image: linear-gradient(to bottom, var(--bevel-highlight), transparent 20%); box-shadow: var(--dialog-depth); color: var(--dialog-panel-foreground); } /* The room going dark: a tint, and the page behind turned down. */ @utility dialog-dim { background-color: var(--dialog-dim); backdrop-filter: brightness(var(--dialog-dim-brightness)); } /* A slotted screw head, lit from above. Each one sits at its own angle. */ @utility dialog-screw { width: var(--dialog-screw-size); height: var(--dialog-screw-size); border-radius: 9999px; background-color: var(--dialog-screw); background-image: linear-gradient(var(--dialog-screw-angle, 30deg), transparent 42%, var(--dialog-screw-slot) 42% 58%, transparent 58%), radial-gradient(circle at 35% 30%, var(--bevel-highlight-strong), transparent 60%); box-shadow: inset 0 -1px 1px var(--bevel-shade), 0 1px 0 var(--bevel-highlight-strong); } /* No two screws turned alike. */ [data-slot="dialog-screws"] > :nth-child(1) { --dialog-screw-angle: 20deg; } [data-slot="dialog-screws"] > :nth-child(2) { --dialog-screw-angle: 75deg; } [data-slot="dialog-screws"] > :nth-child(3) { --dialog-screw-angle: 130deg; } [data-slot="dialog-screws"] > :nth-child(4) { --dialog-screw-angle: 160deg; } /* Keys in the footer: neutral, or red for the destructive one. */ [data-slot="dialog-close"], [data-slot="alert-dialog-cancel"], [data-slot="alert-dialog-action"] { --bevel-material: var(--dialog-key); --bevel-material-edge: var(--dialog-key-edge); --dialog-key-ink: var(--dialog-key-foreground); } [data-tone="destructive"]:is([data-slot="dialog-close"], [data-slot="alert-dialog-action"]) { --bevel-material: var(--dialog-danger); --bevel-material-edge: var(--dialog-danger-edge); --dialog-key-ink: var(--dialog-danger-foreground); }Import the stylesheets after Tailwind in your global CSS, foundation first.
globals.css @import "./bevel.css"; @import "./dialog.css";
Anatomy
Each part is its own component. Leave out the ones you don't need.
<Dialog>
<DialogTrigger />
<DialogContent>
<DialogHeader>
<DialogTitle />
<DialogDescription />
</DialogHeader>
<DialogBody />
<DialogFooter>
<DialogClose />
</DialogFooter>
</DialogContent>
</Dialog>
<AlertDialog>
<AlertDialogTrigger />
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle />
<AlertDialogDescription />
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel />
<AlertDialogAction />
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>Parts
| Part | Renders | What it does |
|---|---|---|
| DialogDialog root | Dialog root | open, defaultOpen, onOpenChange, modal. Escape, the backdrop and Close put it away; focus returns to the trigger. |
| DialogTriggerbutton | button | Opens the dialog. Pass render to use your own button. |
| DialogContentDialog popup | Dialog popup | Dims the room and raises the panel. No padding. align: center or top (for a palette). fasteners: screws (default), stitch, none. showCloseButton (default true). Width from --dialog-width; override with className. |
| DialogHeaderdiv | div | Title and description, padded, clear of the corner ×. |
| DialogTitleh2 | h2 | Names the dialog for screen readers. Required; hide it with sr-only if it must not show. |
| DialogDescriptionp | p | Muted text under the title. |
| DialogBodydiv | div | Padded; scrolls when the panel runs out of room. |
| DialogFooterdiv | div | A ledge behind an engraved rule, holding the keys. Stacks on small screens. |
| DialogClosebutton | button | A raised key that closes. tone: default or destructive. |
| AlertDialogAlert dialog root | Alert dialog root | Doesn't close on an outside click. |
| AlertDialogContentAlert dialog popup | Alert dialog popup | The same panel, narrower (--dialog-alert-width), with no corner ×. fasteners. |
| AlertDialogCancelbutton | button | The safe answer. Put it first: it takes focus. Says “Cancel” by default. |
| AlertDialogActionbutton | button | Does it and closes; onClick runs first. A red key by default (tone="destructive"). |