Dialog
A dialog is a window overlaid on either the primary window or another dialog window. Content behind a modal dialog is inert, meaning that users cannot interact with it.
Edit profile
Make changes to your profile here. Click save when you are done.
Features
- Supports modal and non-modal modes.
- Focus is trapped and scrolling is blocked in the modal mode.
- Provides screen reader announcements via rendered title and description.
- Pressing
Esc
closes the dialog.
Installation
To use the dialog machine in your project, run the following command in your command line:
npm install @zag-js/dialog @zag-js/react # or yarn add @zag-js/dialog @zag-js/react
npm install @zag-js/dialog @zag-js/solid # or yarn add @zag-js/dialog @zag-js/solid
npm install @zag-js/dialog @zag-js/vue # or yarn add @zag-js/dialog @zag-js/vue
npm install @zag-js/dialog @zag-js/vue # or yarn add @zag-js/dialog @zag-js/vue
This command will install the framework agnostic dialog logic and the reactive utilities for your framework of choice.
Anatomy
To use the dialog component correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-part
attribute to help identify them in the DOM.
Usage
First, import the dialog package into your project
import * as dialog from "@zag-js/dialog"
The dialog package exports two key functions:
machine
— The state machine logic for the dialog widget as described in WAI-ARIA specification.connect
— The function that translates the machine's state to JSX attributes and event handlers.
You'll need to provide a unique
id
to theuseMachine
hook. This is used to ensure that every part has a unique identifier.
Next, import the required hooks and functions for your framework and use the dialog machine in your project 🔥
import * as dialog from "@zag-js/dialog" import { useMachine, normalizeProps, Portal } from "@zag-js/react" export function Dialog() { const [state, send] = useMachine(dialog.machine({ id: "1" })) const api = dialog.connect(state, send, normalizeProps) return ( <> <button {...api.triggerProps}>Open Dialog</button> {api.isOpen && ( <Portal> <div {...api.backdropProps} /> <div {...api.positionerProps}> <div {...api.contentProps}> <h2 {...api.titleProps}>Edit profile</h2> <p {...api.descriptionProps}> Make changes to your profile here. Click save when you are done. </p> <div> <input placeholder="Enter name..." /> <button>Save</button> </div> <button {...api.closeTriggerProps}>Close</button> </div> </div> </Portal> )} </> ) }
import * as dialog from "@zag-js/dialog" import { Portal } from "solid-js/web" import { useMachine, normalizeProps } from "@zag-js/solid" import { createMemo, createUniqueId, Show } from "solid-js" export default function Page() { const [state, send] = useMachine(dialog.machine({ id: createUniqueId() })) const api = createMemo(() => dialog.connect(state, send, normalizeProps)) return ( <> <button {...api().triggerProps}>Open Dialog</button> <Show when={api().isOpen}> <Portal> <div {...api().backdropProps} /> <div {...api().positionerProps}> <div {...api().contentProps}> <h2 {...api().titleProps}>Edit profile</h2> <p {...api().descriptionProps}> Make changes to your profile here. Click save when you are done. </p> <button {...api().closeTriggerProps}>X</button> <input placeholder="Enter name..." /> <button>Save Changes</button> </div> </div> </Portal> </Show> </> ) }
import * as dialog from "@zag-js/dialog" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, defineComponent, h, Fragment, Teleport } from "vue" export default defineComponent({ name: "Dialog", setup() { const [state, send] = useMachine(dialog.machine({ id: "1" })) const apiRef = computed(() => dialog.connect(state.value, send, normalizeProps), ) return () => { const api = apiRef.value return ( <> <button {...api.triggerProps}>Open Dialog</button> {api.isOpen && ( <Teleport to="body"> <div {...api.backdropProps} /> <div {...api.positionerProps}> <div {...api.contentProps}> <h2 {...api.titleProps}>Edit profile</h2> <p {...api.descriptionProps}> Make changes to your profile here. Click save when you are done. </p> <button {...api.closeTriggerProps}>X</button> <input placeholder="Enter name..." /> <button>Save Changes</button> </div> </div> </Teleport> )} </> ) } }, })
<script setup> import * as dialog from "@zag-js/dialog"; import { normalizeProps, useMachine } from "@zag-js/vue"; import { computed, Teleport } from "vue"; const [state, send] = useMachine(dialog.machine({ id: "1" })); const api = computed(() => dialog.connect(state.value, send, normalizeProps)); </script> <template> <button ref="ref" v-bind="api.triggerProps">Open Dialog</button> <Teleport to="body"> <div v-if="api.isOpen"> <div v-bind="api.backdropProps" /> <div v-bind="api.positionerProps"> <div v-bind="api.contentProps"> <h2 v-bind="api.titleProps">Edit profile</h2> <p v-bind="api.descriptionProps"> Make changes to your profile here. Click save when you are done. </p> <button v-bind="api.closeTriggerProps">X</button> <input placeholder="Enter name..." /> <button>Save Changes</button> </div> </div> </div> </Teleport> </template>
Managing focus within the dialog
When the dialog opens, it automatically sets focus on the first focusable elements and traps focus within it, so that tabbing is constrained to it.
To control the element that should receive focus on open, pass the
initialFocusEl
context (which can be an element or a function that returns an
element)
export function Dialog() { // initial focused element ref const inputRef = useRef(null) const [state, send] = useMachine( dialog.machine({ initialFocusEl: () => inputRef.current, }), ) // ... return ( //... <input ref={inputRef} /> // ... ) }
export function Dialog() { // initial focused element signal const [inputEl, setInputEl] = createSignal() const [state, send] = useMachine( dialog.machine({ initialFocusEl: inputEl, }), ) // ... return ( //... <input ref={setInputEl} /> // ... ) }
export default defineComponent({ name: "Dialog", setup() { // initial focused element ref const inputRef = ref(null) const [state, send] = useMachine( dialog.machine({ initialFocusEl: () => inputRef.value, }), ) // ... return () => ( //... <input ref={inputRef} /> // ... ) }, })
<script setup> import { ref } from "vue"; // initial focused element ref const inputRef = ref(null); const [state, send] = useMachine( dialog.machine({ initialFocusEl: () => inputRef.value, }) ); </script> <template> <input ref="inputRef" /> </template>
To set the element that receives focus when the dialog closes, pass the
finalFocusEl
in the similar fashion as shown above.
Closing the dialog on interaction outside
By default, the dialog closes when you click its overlay. You can set
closeOnInteractOutside
to false
if you want the modal to stay visible.
const [state, send] = useMachine( dialog.machine({ closeOnInteractOutside: false, }), )
You can also customize the behavior by passing a function to the
onInteractOutside
context and calling event.preventDefault()
const [state, send] = useMachine( dialog.machine({ onInteractOutside(event) { const target = event.target if (target?.closest("<selector>")) { return event.preventDefault() } }, }), )
Listening for open state changes
When the dialog is opened or closed, the onOpenChange
callback is invoked.
const [state, send] = useMachine( dialog.machine({ onOpenChange(details) { // details => { open: boolean } console.log("open:", details.open) }, }), )
Controlling the scroll behavior
When the dialog is open, it prevents scrolling on the body
element. To disable
this behavior, set the preventScroll
context to false
.
const [state, send] = useMachine( dialog.machine({ preventScroll: false, }), )
Creating an alert dialog
The dialog has support for dialog and alert dialog roles. It's set to dialog
by default. To change it's role, pass the role: alertdialog
property to the
machine's context.
That's it! Now you have an alert dialog.
const [state, send] = useMachine( dialog.machine({ role: "alertdialog", }), )
By definition, an alert dialog will contain two or more action buttons. We recommended setting focus to the least destructive action via
initialFocusEl
Styling guide
Earlier, we mentioned that each accordion part has a data-part
attribute added
to them to select and style them in the DOM.
[data-part="trigger"] { /* styles for the trigger element */ } [data-part="backdrop"] { /* styles for the backdrop element */ } [data-part="positioner"] { /* styles for the positioner element */ } [data-part="content"] { /* styles for the content element */ } [data-part="title"] { /* styles for the title element */ } [data-part="description"] { /* styles for the description element */ } [data-part="close-trigger"] { /* styles for the close trigger element */ }
Open and closed state
The dialog has two states: open
and closed
. You can use the data-state
attribute to style the dialog or trigger based on its state.
[data-part="content"][data-state="open|closed"] { /* styles for the open state */ } [data-part="trigger"][data-state="open|closed"] { /* styles for the open state */ }
Methods and Properties
Machine Context
The dialog machine exposes the following context properties:
ids
Partial<{ trigger: string; positioner: string; backdrop: string; content: string; closeTrigger: string; title: string; description: string; }>
The ids of the elements in the dialog. Useful for composition.trapFocus
boolean
Whether to trap focus inside the dialog when it's openedpreventScroll
boolean
Whether to prevent scrolling behind the dialog when it's openedmodal
boolean
Whether to prevent pointer interaction outside the element and hide all content below itinitialFocusEl
HTMLElement | (() => HTMLElement)
Element to receive focus when the dialog is openedfinalFocusEl
HTMLElement | (() => HTMLElement)
Element to receive focus when the dialog is closedrestoreFocus
boolean
Whether to restore focus to the element that had focus before the dialog was openedonOpenChange
(details: OpenChangeDetails) => void
Callback to be invoked when the dialog is opened or closedcloseOnInteractOutside
boolean
Whether to close the dialog when the outside is clickedcloseOnEscape
boolean
Whether to close the dialog when the escape key is pressedonEscapeKeyDown
(event: KeyboardEvent) => void
Callback to be invoked when the escape key is pressedaria-label
string
Human readable label for the dialog, in event the dialog title is not renderedrole
"dialog" | "alertdialog"
The dialog's roleopen
boolean
Whether the dialog is openopen.controlled
boolean
Whether the dialog is controlled by the userdir
"ltr" | "rtl"
The document's text/writing direction.id
string
The unique identifier of the machine.getRootNode
() => Node | ShadowRoot | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.onPointerDownOutside
(event: PointerDownOutsideEvent) => void
Function called when the pointer is pressed down outside the componentonFocusOutside
(event: FocusOutsideEvent) => void
Function called when the focus is moved outside the componentonInteractOutside
(event: InteractOutsideEvent) => void
Function called when an interaction happens outside the component
Machine API
The dialog api
exposes the following methods:
open
boolean
Whether the dialog is opensetOpen
(open: boolean) => void
Function to open or close the dialog
Accessibility
Adheres to the Alert and Message Dialogs WAI-ARIA design pattern.
Keyboard Interactions
- EnterWhen focus is on the trigger, opens the dialog.
- TabMoves focus to the next focusable element within the content. Focus is trapped within the dialog.
- Shift + TabMoves focus to the previous focusable element. Focus is trapped within the dialog.
- EscCloses the dialog and moves focus to trigger or the defined final focus element
Edit this page on GitHub