Dialog
A modal overlay for focused tasks and multi-step flows.
Show codeHide code
Installation
import {
Dialog,
DialogTrigger,
DialogContent,
DialogCloseGroup,
DialogToolbar,
DialogPanes,
DialogAside,
DialogMain,
DialogHeader,
DialogProgression,
DialogHeading,
DialogTitle,
DialogDescription,
DialogBody,
DialogActions,
DialogInfo,
DialogInfoIcon,
DialogClose,
} from "@makeplane/propel/components/dialog";Usage
Compose DialogCloseGroup (corner dismiss), optional DialogToolbar (top chrome with a bottom
border), DialogMain (progression / heading / optional body slot), and DialogActions (optional
DialogInfo + buttons) inside DialogContent. Put DialogDescription in DialogHeading under
the title — not in DialogBody. When DialogBody content can overflow, pass tabIndex={0} so the
scroll region is keyboard-reachable.
import { Button } from "@makeplane/propel/components/button";
import {
Dialog,
DialogActions,
DialogBody,
DialogClose,
DialogCloseGroup,
DialogContent,
DialogDescription,
DialogHeader,
DialogHeading,
DialogInfo,
DialogInfoIcon,
DialogMain,
DialogProgression,
DialogTitle,
DialogTrigger,
} from "@makeplane/propel/components/dialog";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { ChevronLeftOutline, InfoOutline, CloseOutline } from "@makeplane/propel/icons";
/**
* Default composition: full Modal anatomy — progression, heading, body slot, info helper, and
* action row.
*/
export default function BasicDemo() {
return (
<Dialog>
<Button
stretch="auto"
variant="secondary"
size="lg"
render={<DialogTrigger />}
label="Create cycle"
/>
<DialogContent>
<DialogCloseGroup>
<IconButton
variant="ghost"
size="xs"
aria-label="Close"
render={<DialogClose />}
icon={<Icon icon={CloseOutline} />}
/>
</DialogCloseGroup>
<DialogMain>
<DialogHeader>
<DialogProgression>
<IconButton
variant="ghost"
size="xs"
aria-label="Back"
icon={<Icon icon={ChevronLeftOutline} />}
/>
Step 1 of 3
</DialogProgression>
<DialogHeading>
<DialogTitle>Create cycle</DialogTitle>
<DialogDescription>
Name the cycle and set its date range. You can add work items in the next step.
</DialogDescription>
</DialogHeading>
</DialogHeader>
<DialogBody>
<div className="min-h-32 rounded-lg bg-surface-2" />
</DialogBody>
</DialogMain>
<DialogActions>
<DialogInfo>
<DialogInfoIcon>
<Icon icon={InfoOutline} />
</DialogInfoIcon>
Drafts save automatically
</DialogInfo>
<Button
stretch="auto"
variant="ghost"
size="md"
render={<DialogClose />}
label="Cancel"
/>
<Button
stretch="auto"
variant="secondary"
size="md"
render={<DialogClose />}
label="Save draft"
/>
<Button
stretch="auto"
variant="primary"
size="md"
render={<DialogClose />}
label="Continue"
/>
</DialogActions>
</DialogContent>
</Dialog>
);
}Anatomy
| Part | Role |
|---|---|
Dialog |
Root context (open state, modality) |
DialogTrigger / DialogClose |
Behavior grafts onto Button / IconButton via render |
DialogContent |
Portal + backdrop + viewport + popup (size defaults to md) |
DialogCloseGroup |
Absolute top-end slot for the dismiss IconButton |
DialogToolbar |
Optional top chrome; bottom border flush to the popup |
DialogPanes / DialogAside |
Optional two-pane row: 280 px start rail beside DialogMain |
DialogMain |
Padded top section; shrinks so the footer stays visible |
DialogProgression |
Optional back + step label row |
DialogHeader / DialogHeading |
Progression + title / description stack (6 px gap) |
DialogTitle / DialogDescription |
Accessible name + supporting copy — keep both in Heading |
DialogBody |
Scrollable slot only (forms, lists) — not description |
DialogActions |
Footer with top border, pinned while the body scrolls |
DialogInfo / DialogInfoIcon |
Optional footer helper |
Examples
Sizes
DialogContent’s size sets the dialog width: xs 480 px, sm 576 px, md 672 px (default),
lg 800 px, xl 1000 px, or full, which fills the viewport minus an 80 px margin on every edge
and pins the height. Every dialog hangs 80 px from the top of the viewport, so a short one opens
where a tall one does, and grows with its content until it is 80 px from the bottom; past that the
body scrolls. The gutter of DialogMain, DialogActions, and DialogAside follows the size
(16 px, 48 px at full). DialogToolbar matches on the start edge; its end edge is 40 px so
content clears the corner dismiss (48 px at full). density="compact" keeps 16 px at every size,
including full. height="fixed" pins 700 px so split chrome does not jump when the active pane
changes.
Show codeHide code
Scrollable body
Tall content scrolls inside DialogBody so the header and footer stay pinned. Pass tabIndex={0}
on the body when it can overflow.
Show codeHide code
Fullscreen wizard
size="full" plus DialogPanes: a DialogAside step rail on the start side, the content pane in
DialogMain, and a DialogInfo note in the footer beside Back / Next. The panes work at any size;
the step list is your own content.
Show codeHide code
Discard guard
Pass discard on the root with when true while the form has unsaved edits. Escape, an outside
press, and the corner close then raise an alert asking to discard; “Keep editing” returns to the
draft, “Discard” closes and calls onDiscard. Closing from your own handler (Post, Save) is never
intercepted.
Show codeHide code
Confirmation
Title, description, and cancel / confirm actions — a short task without a body slot.
Show codeHide code
Non-dismissable
With disablePointerDismissal on the root, a backdrop click no longer closes the dialog — it
dismisses only via an explicit DialogClose or Escape.
Show codeHide code
Nested dialogs
A DialogTrigger inside a dialog stacks a second dialog above it. The child paints its own
backdrop over the parent. Escape unwinds one level at a time.
Show codeHide code
Open from a menu
Choosing a menu item closes the menu, so the dialog lives outside it and is opened imperatively via
controlled open / onOpenChange.
Show codeHide code
Detached triggers
createDialogHandle() links triggers to a Dialog defined elsewhere in the tree, so several launch
points share one dialog without lifting state.
Show codeHide code
API Reference
Dialog
The dialog Root — Base UI's context/state provider (renders no element of its own). Modal by
default; pass disablePointerDismissal to keep it open on outside clicks. A behavior-only role,
so it lives in components (rules 1a, 2); the styled parts live in elements/dialog and are
grafted onto Base UI behavior here.
DialogTrigger
The behavior that opens the dialog. Use as the render target of a Button so the styled
primitive's look wins via render-composition:
``tsx
<Button variant="secondary" size="lg" label="Open dialog" render={<DialogTrigger />} />;
Base UI manages aria-haspopup/aria-expanded and focus restoration when the dialog closes.
Maps 1:1 to Dialog.Trigger`.
DialogContent
Convenience that composes the dialog overlay boilerplate — portal, shared Backdrop, the
top-anchored DialogViewport, and the DialogPopup — so a consumer only writes the trigger and
the popup body. Each Base UI behavior part grafts onto its styled element via render.
Nested dialogs keep their own dim: Base UI omits a nested Backdrop unless forceRender is set,
which would leave a child modal sitting on the parent with no extra overlay. Closed
force-rendered backdrops set pointer-events: none so they cannot steal clicks from the parent
while unmounting. DialogPanesContext is reset here so a nested dialog does not inherit pane
fill from a parent DialogPanes through the portal.
DialogToolbar
Top chrome of the dialog popup, sibling of DialogMain. Its start gutter follows the size
chosen on DialogContent (16 px, 48 px at full); the end gutter is 40 px so an end-aligned
control clears the corner dismiss. density="compact" keeps 16 px start at full. Bottom border
is flush to the popup, matching DialogActions.
DialogTitle
The accessible title for the dialog: Base UI's Dialog.Title behavior (links to the popup via
aria-labelledby) grafted onto the dialog-owned title chrome (text-h5-medium).
DialogDescription
The supporting description for the dialog: Base UI's Dialog.Description behavior (links to the
popup via aria-describedby) grafted onto the dialog-owned description chrome (text-body-xs
tertiary).
DialogClose
The behavior that closes the dialog when activated. Use as the render target of a Button or
IconButton so the styled primitive's look wins via render-composition. Corner dismiss belongs
in DialogCloseGroup:
``tsx
<DialogCloseGroup>
<IconButton
variant="ghost"
size="xs"
aria-label={closeLabel}
icon={<Icon icon={X} />}
render={<DialogClose />}
/>
</DialogCloseGroup>;
Maps 1:1 to Dialog.Close`.
DialogPanes
The side-by-side row of a two-pane dialog: DialogAside on the start side, DialogMain filling
the rest. Marks its descendants so the ready-made DialogMain fills the row (rule 7).
DialogAside
A 280 px start-side column inside DialogPanes, beside DialogMain. Stretches to the panes'
height so its end seam is the full popup. Holds navigation for the dialog's content — a step
list, a section list. Its gutter follows the size chosen on DialogContent (16 px, 48 px at
full); density="compact" keeps 16 px at full. Pass tabIndex={0} when its content can
overflow.
DialogMain
The padded top section of the dialog popup: optional DialogProgression, DialogHeader /
DialogHeading, and the scrollable DialogBody. Its gutter follows the size chosen on
DialogContent (16 px, 48 px at full); density="compact" keeps 16 px at full. Inside
DialogPanes it fills the width beside DialogAside.
DialogCloseGroup
Positions the dismiss control at the popup's top inline-end corner. Hold an IconButton (size
xs, variant ghost) with render={<DialogClose />} so the button chrome wins and Base UI
supplies close behavior.
DialogProgression
Optional step row at the top of DialogHeader: compose a back IconButton (size xs, variant
ghost) and the step label as children. Caption-md tertiary chrome is baked in. Laid out in
normal flow (not Figma's absolute hang into the gutter) — the glyph sits on the Main padding
edge; consumers supply the back handler.
DialogHeader
The top block inside DialogMain: stacks an optional DialogProgression over a DialogHeading
(title + optional description). Dismiss lives in DialogCloseGroup on the popup, not here — this
part carries extra end padding so progression/title clear the absolute close without widening
Main/Body gutters. Full-width header controls (e.g. a command-palette search field) skip this
part and sit directly in DialogMain so they keep equal side gutters.
DialogHeading
The heading block: stacks the DialogTitle over an optional DialogDescription with a tight
gap. Sit inside DialogHeader (optionally under DialogProgression). Keep supporting copy here
— not in DialogBody — so title/description use the 6 px heading rhythm.
DialogBody
The scrollable main slot inside DialogMain. Grows to fill available space and scrolls
vertically when content exceeds the popup's max-height. overflow-x-clip on this node keeps
trackpad deltas from panning sideways; an inner width clipper keeps focus scrolling from shifting
the body under sticky header/footer. A 2 px padded bleed keeps field ring-2 focus chrome from
being masked by the vertical clip. Pass form fields, lists, or other slot content — keep
DialogDescription in DialogHeading.
When the body can overflow, pass tabIndex={0} so the scroll region is keyboard-reachable (axe
scrollable-region-focusable).
DialogActions
The footer of the dialog popup: an optional DialogInfo at the inline-start, then action
buttons. Its gutter follows the size chosen on DialogContent (16 px, 48 × 18 px at full);
density="compact" keeps 16 px at full. Stays visible while DialogBody scrolls.
DialogInfo
Optional footer helper at the inline-start of DialogActions. Compose a DialogInfoIcon and the
helper label as children. Flexes to push action buttons to the inline-end.
DialogInfoIcon
Decorative 14 px icon slot inside DialogInfo. Bakes aria-hidden and sizes its child via the
shared node-slot. Pass a Propel glyph (or <Icon icon={…} />) as children.