Dialog
A modal panel for a focused sub-task: a titled top bar, a scrolling body and an action bar, framed and never dimmed.
import {
Dialog,
DialogActions,
DialogBody,
DialogClose,
DialogDescription,
DialogEyebrow,
DialogPopup,
DialogTitle,
DialogTopBar,
DialogTrigger,
} from '@fairgarden-private/design/components/Dialog'
The panel renders in a portal as a white scope that follows the page mode, whatever the trigger's ground. Its --ds-stroke-3 frame carries the boundary in both modes; there is no tinted backdrop, so the page stays visible, inert and scroll-locked. Below 768 px the panel is a full-screen sheet with the actions docked at its foot; from 768 px it is centered. It opens instantly, or with a clip reveal where motion is allowed. Focus moves to the first field in the body. An open dialog prints alone.
Use a dialog for a short form or a detail view, never for marketing or exit-intent prompts. Put the solid action first, then its outline twin.
A focused task
A focused task
The top bar holds an optional eyebrow, the title (the dialog's accessible name) and the close X. DialogClose also closes from an action: pass the action Button as render.
Current name: Ridge Loop
'use client'
import * as React from 'react'
import { Button } from '@fairgarden-private/design/components/Button'
import {
Dialog,
DialogActions,
DialogBody,
DialogClose,
DialogDescription,
DialogEyebrow,
DialogPopup,
DialogTitle,
DialogTopBar,
DialogTrigger,
} from '@fairgarden-private/design/components/Dialog'
import styles from './basic.module.css'
export function DialogBasic() {
const [name, setName] = React.useState('Ridge Loop')
const [draft, setDraft] = React.useState(name)
return (
<div className={styles.stack}>
<Dialog
onOpenChange={(open) => {
if (open) setDraft(name)
}}
>
<DialogTrigger variant="outline">Rename Trail</DialogTrigger>
<DialogPopup>
<DialogTopBar>
<DialogEyebrow>Trail 12</DialogEyebrow>
<DialogTitle>Rename Trail</DialogTitle>
<DialogClose />
</DialogTopBar>
<DialogBody>
<DialogDescription>
The new name shows on the map, the trailhead sign list and the printed guide.
</DialogDescription>
<label className={styles.field}>
<span className={styles.label}>Trail Name</span>
<input
className={styles.input}
value={draft}
onChange={(event) => setDraft(event.target.value)}
/>
</label>
</DialogBody>
<DialogActions>
<DialogClose
render={
<Button variant="solid" onClick={() => setName(draft.trim() || name)}>
Save Name
</Button>
}
/>
<DialogClose render={<Button variant="outline">Cancel</Button>} />
</DialogActions>
</DialogPopup>
</Dialog>
<p className={styles.status} aria-live="polite">
Current name: {name}
</p>
</div>
)
}
Wide, with a solid cover
Wide, with a solid cover
wide sets the detail-view width. cover names the page ground's partner preset (paper → forest, white → night); from 768 px it renders as a fully opaque Ground that hides the page, never a tint. When the body overflows, a rule marks each edge with hidden content.
Not in your trip yet.
'use client'
import * as React from 'react'
import { Button } from '@fairgarden-private/design/components/Button'
import {
Dialog,
DialogActions,
DialogBody,
DialogClose,
DialogDescription,
DialogEyebrow,
DialogPopup,
DialogTitle,
DialogTopBar,
DialogTrigger,
} from '@fairgarden-private/design/components/Dialog'
import styles from './cover.module.css'
const stops = [
['Trailhead kiosk', 'Maps, a water tap and the day’s closures.'],
['Alder crossing', 'A plank bridge over the creek; slippery after rain.'],
['Meadow overlook', 'Bench and interpretive sign on the spring bird count.'],
['Old orchard', 'Heritage apples, fenced from deer since 2019.'],
['Ridge saddle', 'The high point at 412 m, with a view to the reservoir.'],
['Fern gully', 'Steep steps down to the spring; hold the rail.'],
['Beaver pond', 'Quiet water; dogs stay on the boardwalk.'],
['Return junction', 'Left for the car park, right for the long loop.'],
] as const
export function DialogCover() {
const [added, setAdded] = React.useState(false)
return (
<div className={styles.stack}>
<Dialog>
<DialogTrigger variant="outline" icon="zoom_in">
Open Trail Guide
</DialogTrigger>
<DialogPopup wide cover="forest">
<DialogTopBar>
<DialogEyebrow>Field Guide</DialogEyebrow>
<DialogTitle>Ridge Loop, Stop by Stop</DialogTitle>
<DialogClose />
</DialogTopBar>
<DialogBody>
<DialogDescription>
Eight stops over 6.4 km. Scroll the list: a rule marks each edge with hidden content.
</DialogDescription>
<ol className={styles.list}>
{stops.map(([name, note]) => (
<li key={name} className={styles.stop}>
<strong className={styles.name}>{name}</strong>
<span>{note}</span>
</li>
))}
</ol>
</DialogBody>
<DialogActions>
<DialogClose
render={
<Button variant="solid" onClick={() => setAdded(true)}>
Add to Trip
</Button>
}
/>
<DialogClose render={<Button variant="outline">Close Guide</Button>} />
</DialogActions>
</DialogPopup>
</Dialog>
<p className={styles.status} aria-live="polite">
{added ? 'Ridge Loop is in your trip.' : 'Not in your trip yet.'}
</p>
</div>
)
}
API Reference
Dialog and DialogTrigger
Groups the parts of a dialog (Base UI Dialog.Root). Modal by default: the page stays visible, inert and scroll-locked, never dimmed [D24 → D121].
Opens the dialog. Renders a Button (§9.2); author its label in title case [D160].
| Prop | Type | Description |
|---|---|---|
butted | | Butts the button against an adjacent field on its |
handle | | Associates a detached trigger with a Dialog created by |
icon | | One optional functional glyph (§6.10), inline tier, FILL 0. |
iconOnly | |
|
iconPosition | | Which side of the label the glyph sits on (a |
onMedia | | Only with |
onPress | | Called on click, after |
payload | | A payload handed to the Dialog’s children function when this trigger opens it. |
primary | | Primary Radix scale, from the primary roster: the outline edge, labels and focus ring. Never defaulted; omitted, it inherits the scope [D133]. |
secondary | | Secondary Radix scale: the |
size | | Fixed height: |
variant | |
|
children | | The label: verb plus object, authored in title case [D160]. |
DialogPopup
The modal panel, rendered in its Base UI Portal as a nested white scope
with the --ds-stroke-3 --primary12 frame. Below --md-n-above it is a
full-screen opaque sheet; from --md-n-above it is centered, at least
--ds-space-40 from every edge. It opens instantly or with a clip reveal,
never a fade. Compose DialogTopBar (eyebrow, title, close), DialogBody
and DialogActions inside it.
Focus moves to the first field in the body, else to Base UI’s default
(the first tabbable element); pass initialFocus to override.
| Prop | Type | Description |
|---|---|---|
cover | | The optional solid cover, from --md-n-above: the page ground’s
companion field [D177] (paper, white, meadow and pollen → |
primary | | Primary Radix scale inside the panel’s |
secondary | | Secondary Radix scale inside the panel: accents only. Omitted, the white preset’s default. |
wide | |
|
container | | The element the portal renders into. Default: |
keepMounted | | Keeps the portal mounted while closed. |
DialogTopBar, DialogEyebrow, DialogTitle and DialogClose
The titled top bar: an optional DialogEyebrow, the DialogTitle and the
DialogClose X. At least --ds-size-control-xl tall; on the sheet a
--ds-stroke-3 --primary12 rule runs under it.
An optional eyebrow above the title, in tracked caps (type-eyebrow); dropped below 360 px.
The dialog’s title and accessible name (Base UI Dialog.Title, an h2),
in type-itemhead. Every dialog has one.
Closes the dialog. By default the top bar’s X: an icon-only Button with
the inline-tier close glyph and a --ds-size-hit target (§9.2). Pass
render (for example <Button variant="outline">Cancel</Button>) to
close from an action instead; label is then ignored.
| Prop | Type | Description |
|---|---|---|
label | | The close X’s accessible name, visually hidden. Default “Close”. |
DialogBody, DialogDescription and DialogActions
The scrolling body: description and content. While content is hidden above or below, a --border-size-2 --role-rule edge shows under the top bar or above the action bar (the §10.19 overflow edge), never a fade.
The dialog’s description (Base UI Dialog.Description), in type-body-ui --primary12.
The action bar: a solid Button first, then its outline twin, each
hugging its label. Stacked and docked at the bottom of the sheet below
--md-n-above; one row from it [D106].
Additional types
DialogActionsProps
Props for DialogActions: div props and render.
type DialogActionsProps = useRender.ComponentProps<'div'>dialogBackdrop
The Backdrop part: colorless by default; covered is the opaque solid cover (from --md-n-above).
type dialogBackdrop = dialogBackdropDialogBodyProps
Props for DialogBody: div props and render.
type DialogBodyProps = useRender.ComponentProps<'div'>DialogCloseProps
Props for DialogClose: Base UI Dialog.Close props plus the X’s accessible name.
type DialogCloseProps = DialogCloseProps & { label?: string }DialogDescriptionProps
Props for DialogDescription: Base UI Dialog.Description props.
type DialogDescriptionProps = DialogDescription.DialogDescriptionPropsDialogEyebrowProps
Props for DialogEyebrow: p props and render.
type DialogEyebrowProps = useRender.ComponentProps<'p'>DialogPopupProps
Props for DialogPopup: Base UI Dialog.Popup props plus the panel axes, the cover and the portal options.
type DialogPopupProps = DialogPopupProps & {
wide?: boolean | null;
primary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null;
secondary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null;
cover?: 'pink' | 'amber' | 'forest' | 'leaf' | 'clay' | 'royal' | 'brick';
container?: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null;
keepMounted?: boolean;
}DialogProps
Props for Dialog: Base UI Dialog.Root props (open, onOpenChange, modal …).
type DialogProps<Payload = unknown> = DialogRoot.Props<Payload>DialogTitleProps
Props for DialogTitle: Base UI Dialog.Title props.
type DialogTitleProps = DialogTitle.DialogTitlePropsDialogTopBarProps
Props for DialogTopBar: div props and render.
type DialogTopBarProps = useRender.ComponentProps<'div'>DialogTriggerProps
Props for DialogTrigger: Button props plus Base UI’s trigger handle and payload.
type DialogTriggerProps = (
| {
iconOnly?: false | null;
onMedia?: false | null;
size?: 'sm' | 'md' | 'lg' | 'xl' | null;
icon?:
| 'menu'
| 'search'
| 'circle'
| 'arrow_forward'
| 'arrow_upward'
| 'expand_more'
| 'close'
| 'remove'
| 'add'
| 'check'
| 'chevron_right'
| 'chevron_left'
| 'more_horiz'
| 'play_arrow'
| 'pause'
| 'download'
| 'zoom_in'
| 'zoom_out'
| 'recenter'
| 'help'
| 'mail';
children?: React.ReactNode;
}
| {
iconOnly: true;
onMedia?: boolean | null;
size?: 'sm' | 'md' | 'lg' | null;
icon:
| 'menu'
| 'search'
| 'circle'
| 'arrow_forward'
| 'arrow_upward'
| 'expand_more'
| 'close'
| 'remove'
| 'add'
| 'check'
| 'chevron_right'
| 'chevron_left'
| 'more_horiz'
| 'play_arrow'
| 'pause'
| 'download'
| 'zoom_in'
| 'zoom_out'
| 'recenter'
| 'help'
| 'mail';
children: React.ReactNode;
}
) & {
/**
* `outline` (default) is the outline twin; `solid` is the page's primary
* action, filled with the scope's action scale; `text` is a text button
* whose glyph trails (§9.2).
*/
variant?: 'text' | 'solid' | 'outline' | null;
/**
* Butts the button against an adjacent field on its `start` or `end`
* edge, as in the butted submit. Default: none.
*/
butted?: 'start' | 'end' | null;
/**
* Primary Radix scale, from the primary roster: the outline edge, labels
* and focus ring. Never defaulted; omitted, it inherits the scope [D133].
*/
primary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null;
/**
* Secondary Radix scale: the `solid` fill and the `text` glyph. Omitted,
* `solid` falls back to the scope's action scale.
*/
secondary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null;
/** Which side of the label the glyph sits on (a `text` Button's chevron trails). */
iconPosition?: 'start' | 'end';
/**
* Called on click, after `onClick`.
* @deprecated Use `onClick`. Kept as an alias from the react-aria Button.
*/
onPress?: PressCallback;
/** Associates a detached trigger with a Dialog created by `Dialog.createHandle`. */
handle?: DialogHandle<unknown>;
/** A payload handed to the Dialog's children function when this trigger opens it. */
payload?: unknown;
}Specification: DESIGN-SYSTEM.md §10.14 (dialog), §10.1 (the overlay surface and backdrop) and §1.3.2 (no scrims).