Accessible base modal with a featured icon, four widths and stacked phone actions; it springs in over a blurred scrim and turns into a drag-to-dismiss bottom sheet below 640px.
dialogmodalalert dialogconfirmsheetoverlayfocus trap
Installation
$npx shadcn@latest add @geturui/dialog
Usage
usage.tsx
1import { Rocket } from "lucide-react";
2import { Button } from "@/components/ui/button";
3import { Dialog } from "@/components/ui/dialog";
4
5export function PublishDialog() {
6 return (
7 <Dialog
8 trigger={<Button>Publish</Button>}
9 icon={<Rocket />}
10 tone="volt"
11 title="Publish to production?"
12 description="Your site updates in about 40 seconds."
13 footer={({ close }) => (
14 <>
15 <Button variant="secondary" onClick={close}>Cancel</Button>
16 <Button onClick={close}>Publish</Button>
17 </>
18 )}
19 />
20 );
21}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
title* | ReactNode | — | Heading; labels the dialog. |
description | ReactNode | — | Supporting copy; describes the dialog. |
children | ReactNode | ({ close }) => ReactNode | — | Body. Scrolls when tall. |
footer | ReactNode | ({ close }) => ReactNode | — | Actions, primary last. Full-width and stacked with the primary on top on phones. |
footerAside | ReactNode | ({ close }) => ReactNode | — | Left side of the footer, e.g. a “Don’t ask again” checkbox. |
icon / tone | ReactNode / "neutral" | "volt" | "success" | "warning" | "danger" | "ember" | — / "neutral" | Featured icon in a tinted tile. It pops in; the warning tone wobbles once. |
layout | "stacked" | "inline" | "centered" | "stacked" | Icon above the text, beside it (from 640px) or centred. |
size | "sm" | "md" | "lg" | "xl" | "md" | Max width 400 / 480 / 640 / 800 px. |
role | "dialog" | "alertdialog" | "dialog" | Use alertdialog for interrupting confirmations. |
closeOnEscape / closeOnScrim / showClose | boolean | true | Dismissal controls. With closeOnScrim off, a scrim click nudges the panel and the sheet can’t be dragged away. |
initialFocus | RefObject<HTMLElement> | — | Element focused on open. Otherwise [data-autofocus], then the first body control, then the footer. |
sheetOnMobile | boolean | true | Below 640px present as a bottom sheet with a drag handle. |
trigger | ReactElement | — | Element that opens the modal; it receives onClick, aria-haspopup and aria-expanded. |
open / defaultOpen / onOpenChange | boolean / boolean / (open: boolean) => void | — | Controlled or uncontrolled open state. |
useDialog() | () => { close } | — | Close the dialog from any descendant. |
Accessibility
- role="dialog" or "alertdialog" with aria-modal, aria-labelledby (title) and aria-describedby (description).
- Focus moves inside on open (initialFocus, [data-autofocus], first control), Tab cycles within the panel, focus that escapes is pulled back, and focus returns to the trigger on close.
- Escape and scrim click close unless disabled; the page behind is scroll-locked without a layout shift.
- Reduced motion swaps the spring, blur and sheet slide for a fade.