Dialog
A modal window for a focused task. Focus is trapped while it is open and returned to the trigger when it closes.
Import
import { Dialog } from "@merid/react";<Dialog.Root>
<Dialog.Trigger asChild>
<Button variant="secondary">Edit profile</Button>
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title>Edit profile</Dialog.Title>
<Dialog.Description>Changes are visible to everyone in your workspace.</Dialog.Description>
<Field label="Display name">
<Input defaultValue="Ada Lovelace" />
</Field>
<Field label="Role">
<Select.Root defaultValue="editor">
<Select.Trigger />
<Select.Content>
<Select.Item value="viewer">Viewer</Select.Item>
<Select.Item value="editor">Editor</Select.Item>
<Select.Item value="admin">Admin</Select.Item>
</Select.Content>
</Select.Root>
</Field>
<Dialog.Footer>
<Dialog.Close>Cancel</Dialog.Close>
<Dialog.Close asChild>
<Button variant="primary">Save</Button>
</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>Anatomy
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Footer>
<Dialog.Close>Cancel</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>- Root holds the open state. It renders no element.
- Trigger is a
<button>that toggles the dialog. PassasChildto render your own element, such as aButton, instead. - Content is the modal surface, portalled to
document.bodywith a backdrop.sizesets its maximum width. - Title labels the dialog (
aria-labelledby). Always include one. - Description is linked with
aria-describedbywhen present. - Footer lays out actions, right-aligned.
- Close with text children renders a secondary Button; use
asChildto supply a different element, such as a primaryButton. Content renders the top-right icon close automatically (showClose, on by default); aClosewith no children renders that icon button yourself instead, and the automatic one steps aside. - Popovers, menus, selects and tooltips opened inside Content layer above the dialog, so a
Selectin a dialog form just works.
Examples
Sizes
size sets the maximum width: sm 440px, md 560px (default), lg 720px, full the viewport minus a 16px margin. At 640px and below every size becomes a bottom sheet.
<Dialog.Content size="sm">…</Dialog.Content>
<Dialog.Content size="md">…</Dialog.Content>
<Dialog.Content size="lg">…</Dialog.Content>
<Dialog.Content size="full">…</Dialog.Content>Controlled
Pass open and onOpenChange to keep the state in your component, for example to open the dialog from somewhere other than a Trigger.
const [open, setOpen] = useState(false);
<Button variant="secondary" onClick={() => setOpen(true)}>Open from state</Button>
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Content>
<Dialog.Title>Controlled dialog</Dialog.Title>
<Dialog.Description>Open state lives in the parent component.</Dialog.Description>
<Dialog.Footer>
<Button variant="primary" onClick={() => setOpen(false)}>Done</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>Initial focus
Focus moves to the element passed as initialFocus, then to an element with data-autofocus, then to the first tabbable element.
<Dialog.Content>
<Dialog.Title>Rename</Dialog.Title>
<Input data-autofocus defaultValue="Northwind" />
</Dialog.Content>Blocking dismissal
Set closeOnOutsidePress={false} or closeOnEscape={false} when losing unsaved work would be costly. Prefer AlertDialog for confirmations.
API reference
Dialog.Root
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | – | Controlled open state. |
defaultOpen | boolean | false | Initial open state when uncontrolled. |
onOpenChange | (open: boolean) => void | – | Called when the open state should change. |
children | ReactNode | – | Trigger, Content and anything else sharing this dialog's state. |
Dialog.Trigger
Accepts every <button> attribute. type defaults to "button" (not set when asChild is used).
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render the single child element (e.g. your own `Button`) instead of a `<button>`, merging props, ref and handlers. |
ref | Ref<HTMLButtonElement> | – | Forwarded ref to the button. |
Dialog.Content
Accepts every <div> attribute except role.
| Prop | Type | Default | Description |
|---|---|---|---|
size | DialogSize | "md" | Maximum width: `sm` 440px, `md` 560px, `lg` 720px, `full` the viewport minus a 16px margin. `DialogSize` is `"sm" | "md" | "lg" | "full"`. |
closeOnOutsidePress | boolean | true | Close when the backdrop is pressed. |
closeOnEscape | boolean | true | Close on Escape. |
initialFocus | RefObject<HTMLElement | null> | – | Element to focus when opened; defaults to [data-autofocus], then the first tabbable element. |
container | Element | null | document.body | Portal target. `undefined` uses `document.body`; `null` renders nothing until the target exists. |
showClose | boolean | true | Render the standard top-right icon close button automatically. Skipped while you render your own icon `Close`. |
closeLabel | string | "Close" | Accessible name of the automatic close button. |
ref | Ref<HTMLDivElement> | – | Forwarded ref to the dialog element. |
Dialog.Title
Renders an <h2>. Accepts every heading attribute.
| Prop | Type | Default | Description |
|---|---|---|---|
ref | Ref<HTMLHeadingElement> | – | Forwarded ref to the heading. |
Dialog.Description
Renders a <p>. Accepts every paragraph attribute.
| Prop | Type | Default | Description |
|---|---|---|---|
ref | Ref<HTMLParagraphElement> | – | Forwarded ref to the paragraph. |
Dialog.Close
Accepts every <button> attribute.
| Prop | Type | Default | Description |
|---|---|---|---|
icon | boolean | true without children | Render the standard top-right icon button. When false (default if children are given) it renders the children as a secondary Button. |
asChild | boolean | false | Render the single child element (e.g. your own `Button`) instead of a `<button>`, merging props, ref and handlers. |
ref | Ref<HTMLButtonElement> | – | Forwarded ref to the button. |
Dialog.Footer
Renders a <div> and accepts every <div> attribute; it has no additional props.
Styling
| Class | Element |
|---|---|
.mrd-dialog__backdrop | Fixed backdrop, var(--mrd-backdrop) |
.mrd-dialog | The surface: max width from data-size (440 / 560 / 720px / full), --mrd-radius-card, --mrd-shadow-2xl |
.mrd-dialog__title | Heading |
.mrd-dialog__description | Supporting text |
.mrd-dialog__footer | Action row |
.mrd-dialog__close | Icon close button |
Content carries data-size; content and backdrop carry data-state="open" while mounted; the trigger carries data-state="open" | "closed". At 640px and below the dialog becomes a bottom sheet and the footer stacks its actions. Tokens used: --mrd-backdrop, --mrd-surface, --mrd-z-overlay, --mrd-duration-enter. Popovers, menus, selects and tooltips opened inside the dialog layer above it (--mrd-z-popover, --mrd-z-tooltip). A Close with text children renders as .mrd-button with data-variant="secondary". To style the Trigger, or a Close as another variant, pass asChild and a Button.
Accessibility
Follows the WAI-ARIA Dialog (Modal) pattern. Content has role="dialog" and aria-modal="true"; the Trigger has aria-haspopup="dialog" and aria-expanded. Page scroll is locked while open.
| Key | Action |
|---|---|
| SpaceEnter | On the trigger, opens the dialog. |
| Tab | Moves focus to the next tabbable element inside the dialog, wrapping at the end. |
| ShiftTab | Moves focus to the previous tabbable element, wrapping at the start. |
| Esc | Closes the dialog and returns focus to the trigger. |
Guidelines
Do
Avoid
Do
Avoid
Related
- AlertDialog for confirmations that need a response.
- Drawer for longer content alongside the page.
- Popover for non-modal, anchored content.