Dialog
A modal dialog or alert dialog with a trigger, title, description, body, footer and close parts. Focus moves in on open and returns to the trigger on close.
Import
import { Dialog } from '@nizivo/react/dialog';
// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css and @nizivo/react/css/icon-button.css
import '@nizivo/react/css/dialog.css';Static or interactive
Client. It always ships React Aria's dialog and overlay code (+61,402 B gzip under Turbopack for
one Dialog on the Next.js fixture). The default Dialog.Close writes the IconButton markup itself,
so it adds no Tooltip code. How to choose.
The trigger
Compose a Button (or an IconButton) into the trigger; it is still the recommended look:
<Dialog.Trigger render={<Button variant="secondary">Edit</Button>} />A Button is 32, 40 or 48 px tall and brings the variants, the focus ring and the pressed state.
A bare <Dialog.Trigger>Edit</Dialog.Trigger> is a plain, unstyled <button>, but it has a minimum
size of its own: at least 24 by 24 CSS pixels, the minimum target of WCAG 2.2 success criterion 2.5.8
(Target Size, Minimum), so two bare triggers side by side, or a lone glyph, still meet it. It also
carries nz-hit-target, so under a coarse pointer it gets the same 44 px hit area as every standalone
pressable. A trigger you render yourself through render keeps its own size and gets neither.
A trigger inside running text, such as a word in a sentence that opens a dialog, is the exception:
render your own element through render there. The 24 px minimum would make its line taller than
the others, and under a coarse pointer the 44 px hit area would reach into the lines above and below
and take taps meant for a link there. WCAG 2.2 2.5.8 exempts a target inside a sentence, so the
inline element needs neither.
The close button
<Dialog.Close /> with no render and no children is an IconButton (tertiary, md) showing the
close glyph, named "Close". Change the name with labels, or pass aria-label:
<Dialog.Popup>
<Dialog.Title>Edit product</Dialog.Title>
<Dialog.Close labels={{ close: 'Fermer' }} />
<Dialog.Body>…</Dialog.Body>
</Dialog.Popup>Placed as a direct child of Dialog.Popup, it sits in the popup's top end corner, 20 px from both
edges, where Figma draws the header's close glyph, and the title leaves room for it. Its focus ring
always shows, because it is positioned against the popup, which never clips. Its markup is
IconButton's, so a page that imports per-component stylesheets needs css/icon-button.css for it too
(the stylesheets list above names it; styles.css already has it).
With children, it is a plain button with that text; with render, it is your element, for example
render={<Button>Cancel</Button>} in the footer, or another glyph through
render={<IconButton icon={<X />} label="Close" tooltip={false} />}. Either one removes the default
glyph. A control placed directly in the content and flush with its edge draws its ring inside itself,
so the content's corner clip never cuts the ring.
Focus return
When the dialog closes, focus goes, in order, to:
returnFocusonDialog.Root, when it is an element ref or a function that returns an element that is still on the page;- the trigger, or the element that had focus when the dialog opened;
- the trigger of the overlay the dialog was opened from: a Dialog opened from a Menu item returns focus to the Menu button, after the Menu has closed;
<body>, and development builds warn in the console, namingreturnFocus. Production builds drop the warning.
returnFocus={false} leaves focus wherever the browser puts it, for an app that moves focus itself.
Focus that already went somewhere (an outside click on another control) is never moved.
const search = useRef<HTMLInputElement>(null);
<Dialog.Root open={open} onOpenChange={setOpen} returnFocus={search}>
…
</Dialog.Root>;Examples
Basic
Figma Modal Type=Destructive: a server Button arrives as the trigger through render. Compose a Button (or an IconButton) into Dialog.Trigger rather than styling the bare part: it brings the control look and the 40 px control height (the bare part has only the WCAG 2.2 2.5.8 minimum of 24 x 24 px, and the 44 px hit area under a coarse pointer; M27). The bare Dialog.Close is the header's close IconButton, named "Close"; the alertdialog still focuses Cancel, its least destructive action.
import type { JSX } from 'react';
import { Button } from '@nizivo/react/button';
import { Dialog } from '@nizivo/react/dialog';
export function DialogExample(): JSX.Element {
return (
<Dialog.Root>
<Dialog.Trigger render={<Button variant="critical">Delete product</Button>} />
<Dialog.Popup role="alertdialog" size="sm">
<Dialog.Title>Delete “Stoneware mug”?</Dialog.Title>
<Dialog.Close />
<Dialog.Description>This can't be undone.</Dialog.Description>
<Dialog.Footer>
<Dialog.Close render={<Button>Cancel</Button>} />
<Button type="submit" variant="critical">
Delete
</Button>
</Dialog.Footer>
</Dialog.Popup>
</Dialog.Root>
);
}
Open
Open on the server: closed overlays and server-rendered opens both emit no popup HTML (§12.2 SSR). The trigger is a composed Button, as everywhere.
Source only
defaultOpen), which would move focus on this page, so it is shown as source only.import type { JSX } from 'react';
import { Button } from '@nizivo/react/button';
import { Dialog } from '@nizivo/react/dialog';
export function DialogOpenExample(): JSX.Element {
return (
<Dialog.Root defaultOpen>
<Dialog.Trigger render={<Button variant="secondary">Edit</Button>} />
<Dialog.Popup>
<Dialog.Title level={3}>Edit product</Dialog.Title>
<Dialog.Close />
<Dialog.Body>Body</Dialog.Body>
</Dialog.Popup>
</Dialog.Root>
);
}
Plain Close
A footer-less dialog closed by a full-width Dialog.Close flush with the popup's bottom edge. The content clips its corners (Figma), so a control placed straight in it draws its keyboard ring inside itself, where the clip cannot cut it (§12.3).
Source only
defaultOpen), which would move focus on this page, so it is shown as source only.import type { JSX } from 'react';
import { Button } from '@nizivo/react/button';
import { Dialog } from '@nizivo/react/dialog';
export function DialogPlainCloseExample(): JSX.Element {
return (
<Dialog.Root defaultOpen>
<Dialog.Trigger render={<Button variant="secondary">Details</Button>} />
<Dialog.Popup>
<Dialog.Title level={3}>Order shipped</Dialog.Title>
<Dialog.Body>It arrives on Friday.</Dialog.Body>
<Dialog.Close>Done</Dialog.Close>
</Dialog.Popup>
</Dialog.Root>
);
}
Parts
Use the parts through the namespace (Dialog.Root) or by their flat names (DialogRoot); both work from Server Components.
Dialog.Root
Client component Renders no element of its own. Props: DialogRootProps. Source: src/dialog/dialog.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | |
defaultOpen | boolean | – | |
onOpenChange | (open: boolean, details: ChangeDetails<OpenReason>) => void | – |
|
open | boolean | – | |
returnFocus | ReturnFocusTarget | true | Where focus goes when the dialog closes (§12.2 Focus return). |
Dialog.Trigger
Without render, its own <button>: nz-hit-target gives it the 44 px coarse-pointer hit area of every standalone pressable, and dialog.css gives that bare button a 24 x 24 px minimum (WCAG 2.2 2.5.8; M27). A composed Button or IconButton keeps its own size and classes.
Client component Renders data-slot="dialog-trigger" with the class nz-dialog-trigger. Props: DialogTriggerProps. Source: src/dialog/dialog.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
children | ReactNode | – | |
render | RenderProp<DialogTriggerState> | – | Must render a |
- Also takes every attribute of
<button>exceptchildren. (Omit<ComponentPropsWithRef<"button">, "children">)
render takes an element, or, in client code, a function of the props and this state (DialogTriggerState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – | |
focusVisiblerequired | boolean | – | |
openrequired | boolean | – | |
pressedrequired | boolean | – |
Dialog.Popup
Client component Renders data-slot="dialog-popup" with the class nz-dialog-popup. Props: DialogPopupProps. Source: src/dialog/dialog.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
backdrop | boolean | – | The scrim element. Defaults to on for the root modal and off for nested modals. |
childrenrequired | ReactNode | – | |
container | Element | null | – | Portal container. Defaults to the parent overlay, else |
role | DialogRole"dialog""alertdialog" | 'dialog' |
|
size | DialogSize"sm""md""lg" | 'md' |
- Also takes every attribute of
<div>exceptchildren,role. (Omit<ComponentPropsWithRef<"div">, "children" | "role">)
Dialog.Title
Client component Renders data-slot="dialog-title" with the class nz-dialog-title. Props: DialogTitleProps. Source: src/dialog/dialog.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
level | DialogTitleLevel23456 | 2 | |
render | RenderProp<Record<string, never>> | – |
- Also takes every attribute of
<h2>exceptid. (Omit<ComponentPropsWithRef<"h2">, "id">)
render takes an element, or, in client code, a function of the props and no state.
Dialog.Description
Client component Renders data-slot="dialog-description" with the class nz-dialog-description. Props: DialogDescriptionProps. Source: src/dialog/dialog.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<Record<string, never>> | – |
- Also takes every attribute of
<p>exceptid. (Omit<ComponentPropsWithRef<"p">, "id">)
render takes an element, or, in client code, a function of the props and no state.
Dialog.Body
The only scroller inside the popup.
Client component Renders data-slot="dialog-body" with the class nz-dialog-body. Props: DialogBodyProps. Source: src/dialog/dialog.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<Record<string, never>> | – |
- Also takes every attribute of
<div>. (ComponentPropsWithRef<"div">)
render takes an element, or, in client code, a function of the props and no state.
Dialog.Footer
Client component Renders data-slot="dialog-footer" with the class nz-dialog-footer. Props: DialogFooterProps. Source: src/dialog/dialog.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<Record<string, never>> | – |
- Also takes every attribute of
<div>. (ComponentPropsWithRef<"div">)
render takes an element, or, in client code, a function of the props and no state.
Dialog.Close
With render or children, the consumer's button. With neither, an IconButton (tertiary, md) showing the core close glyph (§12.3, §14.2), named by labels.close. Its markup is the IconButton element's (iconButtonElement, icon-button-static.tsx since M31), written out: importing that builder cost the Dialog entry about 280 B gzip against its 31 KiB cap. An overlay-lane case compares the two.
Client component Renders data-slot="dialog-close" with the class nz-dialog-close. Props: DialogCloseProps. Source: src/dialog/dialog.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
children | ReactNode | – | Text content, rendered in a plain |
labels | Partial<Pick<DialogLabels, "close">> | – | The default IconButton's accessible name; an |
render | RenderProp<DialogCloseState> | – | Must render a |
- Also takes every attribute of
<button>exceptchildren. (Omit<ComponentPropsWithRef<"button">, "children">)
render takes an element, or, in client code, a function of the props and this state (DialogCloseState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – | |
focusVisiblerequired | boolean | – | |
pressedrequired | boolean | – |
Other exports
DialogBodyPropstypeimport type { DialogBodyProps } from '@nizivo/react/dialog'The props of
Dialog.Body, listed above.DialogClosePropstypeimport type { DialogCloseProps } from '@nizivo/react/dialog'The props of
Dialog.Close, listed above.DialogCloseStatetypeimport type { DialogCloseState } from '@nizivo/react/dialog'The render state of
Dialog.Close, listed above.DialogDescriptionPropstypeimport type { DialogDescriptionProps } from '@nizivo/react/dialog'The props of
Dialog.Description, listed above.DialogFooterPropstypeimport type { DialogFooterProps } from '@nizivo/react/dialog'The props of
Dialog.Footer, listed above.DialogLabelstypeimport type { DialogLabels } from '@nizivo/react/dialog'Strings the Dialog renders (§9.3 rule 7, §16.4).
closeis the accessible name of the defaultDialog.Close(an IconButton showing the close glyph), set through that part'slabels. Engine-owned with React Aria (not overridable):dismiss, the visually hidden "Dismiss" button rendered inside a dismissable dialog.Name Type Default Description closerequiredstring'Close'dismissrequiredstring– DialogPopupPropstypeimport type { DialogPopupProps } from '@nizivo/react/dialog'The props of
Dialog.Popup, listed above.DialogPopupStatetypeimport type { DialogPopupState } from '@nizivo/react/dialog'Name Type Default Description enteringrequiredboolean– exitingrequiredboolean– DialogRoletypeimport type { DialogRole } from '@nizivo/react/dialog'Figma Modal
Type: Default →dialog, Destructive →alertdialog.type DialogRole = "dialog" | "alertdialog"DialogRootPropstypeimport type { DialogRootProps } from '@nizivo/react/dialog'The props of
Dialog.Root, listed above.DialogSizetypeimport type { DialogSize } from '@nizivo/react/dialog'mdis Figma's only Modal width (480 px);sm(400) andlg(640) are provisional.type DialogSize = "sm" | "md" | "lg"DialogTitleLeveltypeimport type { DialogTitleLevel } from '@nizivo/react/dialog'type DialogTitleLevel = 2 | 3 | 4 | 5 | 6DialogTitlePropstypeimport type { DialogTitleProps } from '@nizivo/react/dialog'The props of
Dialog.Title, listed above.DialogTriggerPropstypeimport type { DialogTriggerProps } from '@nizivo/react/dialog'The props of
Dialog.Trigger, listed above.DialogTriggerStatetypeimport type { DialogTriggerState } from '@nizivo/react/dialog'The render state of
Dialog.Trigger, listed above.
Data attributes and slots
Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.
| Slot | Rendered by | Data attributes |
|---|---|---|
dialog-backdrop.nz-dialog-backdrop | Dialog.Popupinner element (CSS only) |
|
dialog-body.nz-dialog-body | Dialog.Bodyroot (takes render) | none |
dialog-close.nz-dialog-close | Dialog.Closeroot (takes render); interactive, focus: self |
|
dialog-content.nz-dialog-content | Dialog.Popupinner element (CSS only) | none |
dialog-description.nz-dialog-description | Dialog.Descriptionroot (takes render) | none |
dialog-footer.nz-dialog-footer | Dialog.Footerroot (takes render) | none |
dialog-popup.nz-dialog-popup | Dialog.Popuproot (no render); forced colours: border |
|
dialog-positioner.nz-dialog-positioner | Dialog.Popupinner element (CSS only) |
|
dialog-title.nz-dialog-title | Dialog.Titleroot (takes render) | none |
dialog-trigger.nz-dialog-trigger | Dialog.Triggerroot (takes render); interactive, focus: self |
|
icon-button-icon.nz-icon-button-icon | Dialog.Closeinner element (CSS only) | none |
CSS hooks
Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.
| Hook | Default | Read as |
|---|---|---|
--nz-dialog-bg | --nz-bg-surface-raised |
|
--nz-dialog-radius | --nz-radius-2xl |
|
--nz-dialog-width |
|
|
Layers: nz.components. Classes: .nz-dialog-backdrop, .nz-dialog-body, .nz-dialog-close, .nz-dialog-content, .nz-dialog-description, .nz-dialog-footer, .nz-dialog-popup, .nz-dialog-positioner, .nz-dialog-title, .nz-dialog-trigger, .nz-hit-target, .nz-icon-button.
Tokens this stylesheet reads (28)
--nz-bg-scrim, --nz-bg-subtle, --nz-bg-surface-raised, --nz-border-width-focus, --nz-duration-base, --nz-duration-fade, --nz-duration-spring, --nz-easing-enter, --nz-easing-exit, --nz-easing-spring, --nz-elevation-4, --nz-font-body-l, --nz-font-heading-h3, --nz-icon-secondary, --nz-radius-2xl, --nz-scale-enter, --nz-size-control-md, --nz-size-icon-md, --nz-space-2, --nz-space-3, --nz-space-4, --nz-space-5, --nz-space-6, --nz-space-8, --nz-text-primary, --nz-text-secondary, --nz-tracking-heading-h3, --nz-z-overlay
Accessibility
These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:
dialog-close: draws the focus outline itself on keyboard focus.dialog-trigger: draws the focus outline itself on keyboard focus.
In forced-colours mode these states keep a visible system colour: dialog-popup (border).
Behaviour comes from the react-aria engine through Nizivo's adapter: focus management, ARIA wiring and keyboard handling are the engine's.
Pattern: WAI-ARIA Authoring Practices.
Keyboard
| Keys | Action |
|---|---|
| Enter, Space | On the trigger (a button), opens the dialog and moves focus to the first focusable element in its body, or to the dialog itself when the body has none. An alertdialog focuses the Close in its footer (its least destructive action), else any Close; a control with autoFocus wins. |
| Tab, Shift+Tab | Moves focus between the focusable elements inside the dialog; focus stays inside while it is open. |
| Escape | Closes the dialog (also an alertdialog) and returns focus to the trigger, or along the returnFocus chain below. |
Source: react-aria-components 1.22.1 ModalOverlay and Modal (react-aria 3.53.1 useModalOverlay, which contains focus) through _engines/react-aria/dialog.client.tsx. React Aria focuses the dialog element; the first-focusable rule is Nizivo's (initialFocus on the engine seam, ARCHITECTURE.md §12.2), checked by the overlay lane (test/overlay/dialog.spec.tsx). apps/docs/scripts/smoke.ts checks opening with Enter, Tab staying inside, and Escape closing with focus back on the trigger; the Next fixture checks Escape and the Close part. An alertdialog does not close on an outside press.
Figma
Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.
- Canonical: Figma node
7275:15139 - Ghost masters: none
Delta
A signed change, such as +12.4%, coloured by direction and read with a visually hidden comparison. The sign carries the direction without colour.
Drawer
A modal panel that slides in from an edge of the viewport, for details or a task beside the page, with a header, a body that scrolls and a footer. side="bottom" is a bottom sheet.