Dialog
A window overlaid on the page, rendering the content underneath inert.
Installation
Install the following dependencies:
Set up the token layer and the StyleX compiler, if you have not already. See
Installation, and copy state-styles.ts from the same page.
Copy states.ts for the shared popup surface and install the
Button for the close control.
Copy and paste the following code into your project.
Update the import paths to match your project setup.
Usage
Composition
DialogContent renders the portal, the overlay, the viewport and the popup in
one part. Pass props to the overlay and the viewport through overlayProps and
viewportProps, or compose DialogPortal, DialogOverlay, DialogViewport yourself.
DialogFooter is drawn as a tray, and the content above it is drawn as a
raised panel with rounded bottom corners and a hairline shadow. Keep the footer
the last child of DialogContent. It extends to the popup's edges and draws the
panel's bottom edge itself, so the panel needs no wrapper.
Custom close button
Use showCloseButton={false} to drop the corner button, then put your own
controls in the footer.
No close button
Use showCloseButton={false} on DialogContent to hide the corner button.
DialogFooter takes showCloseButton too, which appends a Close button to
the row.
Scrollable content
A dialog taller than the window scrolls in its viewport. Nothing is clipped, and you do not need to set a height.
Sticky footer
To scroll the body instead, clamp maxHeight on the content through xstyle
and give the body overflow-y: auto. The content is a grid. Set
gridTemplateRows so the body is the row that shrinks to fit the clamp.
Imperative API
defineDialog builds a dialog with no trigger in the tree. Call
open(payload) from any event handler; the payload is typed and is passed to the
render function.
Define it at module scope
defineDialog<Payload>(render) returns { open, close, Root }. Keep it in a
module so any caller can import it.
Put state in the body
The render function runs only while the dialog is on screen, so state declared inside it starts fresh on every open.
Mount the root once
Open it from anywhere
API reference
Pass render to replace a part's element or compose it with another component.
Every part wraps the Base UI Dialog part of the same name and accepts its
props. See the Base UI reference
for the full list. Each styled part also takes xstyle and className.
Dialog
| Prop | Type | Default |
|---|---|---|
open | boolean | — |
defaultOpen | boolean | false |
onOpenChange | (open: boolean, details) => void | — |
modal | boolean | "trap-focus" | true |
disablePointerDismissal | boolean | false |
handle | DialogHandle | — |
Call details.cancel() inside onOpenChange to block a close. Use this to
write an unsaved-changes guard.
DialogContent
| Prop | Type | Default |
|---|---|---|
showCloseButton | boolean | true |
overlayProps | DialogOverlayProps | — |
viewportProps | DialogViewportProps | — |
initialFocus | boolean | RefObject | (type) => … | — |
finalFocus | boolean | RefObject | (type) => … | — |
DialogFooter
| Prop | Type | Default |
|---|---|---|
showCloseButton | boolean | false |
defineDialog
| Member | Type |
|---|---|
open(payload) | (payload: Payload) => void |
close() | () => void |
Root | every Dialog prop except open, defaultOpen, handle and children |
Mount Root somewhere that stays mounted across navigation. It renders into
<body> through a portal, so its place in the tree only decides when it mounts
and unmounts.
Data attributes
| Attribute | On | Effect |
|---|---|---|
data-slot="dialog-content" | the popup | Popup slot |
data-slot="dialog-overlay" | the scrim | Backdrop surface |
data-starting-style, data-ending-style | popup and overlay | Base UI writes them while the dialog animates |
data-open, data-closed | popup and overlay | Base UI writes them |
data-nested, data-nested-dialog-open | popup | Set when dialogs are stacked |