Overlays & feedback
Modal dialog
A focused overlay for a single decision or a short task — confirmations, destructive actions, and quick forms. Dims the page behind a scrim, traps focus, and closes on Escape or scrim click.
npx shadcn@latest add @orbit/modalOverview
Live — click to open a real dialog. Each closes on Escape, the scrim, Cancel, or the × in the corner.
Anatomy
A dialog is a scrim plus a card with three regions: a header (optional tinted icon, title, subtitle, close ×), a body that scrolls independently when tall, and a footer of actions aligned right. The card is centered, rounded 14px, elevated, and animates in with a subtle rise and fade.
Common dialogs
The dialogs a project app needs most — all live.
Sizes
Match the width to the content: sm 400px for confirmations, md 496px for short forms, lg 580px and xl 680px for richer flows. Max height is 100vh − 48px.
Long content
The body scrolls on its own while the header and footer stay pinned, so actions are always reachable.
Dismissal
By default the scrim, Escape, the ×, and Cancel all close the dialog. Set dismissable={false} to ignore scrim clicks when an accidental tap would lose meaningful work — Escape and explicit buttons still work.
Props
<Modal>
| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | — | Controlled open state. |
| defaultOpen | boolean | false | Initial open state when uncontrolled. |
| onOpenChange | (open: boolean) => void | — | Called when the dialog opens or closes. |
| dismissable | boolean | true | false → scrim clicks no longer close the dialog; Escape still does. |
<ModalContent>
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "sm" | "md" | "lg" | "xl" | "md" | Card width: 400 / 496 / 580 / 680px. |
| keepMounted | boolean | false | Keep the dialog mounted in the DOM while closed. |
| …rest | HTMLAttributes<div> | — | Forwarded to the dialog card, including ref. |
<ModalHeader>
| Prop | Type | Default | Description |
|---|---|---|---|
| icon | ReactNode | — | 22px glyph rendered in a 38px tinted square before the title. |
| tone | "neutral" | "blue" | "red" | "green" | "amber" | "blue" | Tint pair for the icon square. |
| showClose | boolean | true | Hide the × for dialogs with a single explicit action. |
<ModalFooter>
| Prop | Type | Default | Description |
|---|---|---|---|
| meta | ReactNode | — | Left-aligned helper text, e.g. “4 of 10 seats used”. |
<ModalTrigger> / <ModalClose>
| Prop | Type | Default | Description |
|---|---|---|---|
| render | ReactElement | — | Render a custom element (usually a Button) instead of the plain <button>. All raw Base UI parts are also exported as ModalPrimitive. |
Usage
Modals interrupt — use them only when focus is warranted.
Reserve modals for one focused task
A confirmation, a destructive action, or a short form — not a place to browse or read.
Stack modals on modals
Opening a dialog from a dialog buries context. Close the first or use a different surface.
Guard destructive actions
Use the red treatment, name the consequence, and require type-to-confirm for the irreversible ones.
Put long content in a modal
If it scrolls a lot or needs deep focus, use a full page or a side panel instead.
Keyboard & accessibility
Built on Base UI Dialog: role='dialog' with aria-modal, focus trapped inside while open and returned to the trigger on close, page scroll locked, and the title/description wired up as the accessible name.