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
Esccloses 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/svelte # or yarn add @zag-js/dialog @zag-js/svelte
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-partattribute 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
idto theuseMachinehook. 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 service = useMachine(dialog.machine, { id: "1" }) const api = dialog.connect(service, normalizeProps) return ( <> <button {...api.getTriggerProps()}>Open Dialog</button> {api.open && ( <Portal> <div {...api.getBackdropProps()} /> <div {...api.getPositionerProps()}> <div {...api.getContentProps()}> <h2 {...api.getTitleProps()}>Edit profile</h2> <p {...api.getDescriptionProps()}> Make changes to your profile here. Click save when you are done. </p> <div> <input placeholder="Enter name..." /> <button>Save</button> </div> <button {...api.getCloseTriggerProps()}>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 service = useMachine(dialog.machine, { id: createUniqueId() }) const api = createMemo(() => dialog.connect(service, normalizeProps)) return ( <> <button {...api().getTriggerProps()}>Open Dialog</button> <Show when={api().open}> <Portal> <div {...api().getBackdropProps()} /> <div {...api().getPositionerProps()}> <div {...api().getContentProps()}> <h2 {...api().getTitleProps()}>Edit profile</h2> <p {...api().getDescriptionProps()}> Make changes to your profile here. Click save when you are done. </p> <button {...api().getCloseTriggerProps()}>X</button> <input placeholder="Enter name..." /> <button>Save Changes</button> </div> </div> </Portal> </Show> </> ) }
<script setup> import * as dialog from "@zag-js/dialog" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, Teleport } from "vue" const service = useMachine(dialog.machine, { id: "1" }) const api = computed(() => dialog.connect(service, normalizeProps)) </script> <template> <button ref="ref" v-bind="api.getTriggerProps()">Open Dialog</button> <Teleport to="body"> <div v-if="api.open"> <div v-bind="api.getBackdropProps()" /> <div v-bind="api.getPositionerProps()"> <div v-bind="api.getContentProps()"> <h2 v-bind="api.getTitleProps()">Edit profile</h2> <p v-bind="api.getDescriptionProps()"> Make changes to your profile here. Click save when you are done. </p> <button v-bind="api.getCloseTriggerProps()">X</button> <input placeholder="Enter name..." /> <button>Save Changes</button> </div> </div> </div> </Teleport> </template>
<script lang="ts"> import * as dialog from "@zag-js/dialog" import { portal, normalizeProps, useMachine } from "@zag-js/svelte" const id = $props.id() const service = useMachine(dialog.machine, ({ id })) const api = $derived(dialog.connect(service, normalizeProps)) </script> <button {...api.getTriggerProps()}>Open Dialog</button> {#if api.open} <div use:portal {...api.getBackdropProps()}></div> <div use:portal {...api.getPositionerProps()}> <div {...api.getContentProps()}> <h2 {...api.getTitleProps()}>Edit profile</h2> <p {...api.getDescriptionProps()}>Make changes to your profile here. Click save when you are done.</p> <div> <input placeholder="Enter name..." /> <button>Save</button> </div> <button {...api.getCloseTriggerProps()}>Close</button> </div> </div> {/if}
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 service = useMachine(dialog.machine, { initialFocusEl: () => inputRef.current, }) // ... return ( //... <input ref={inputRef} /> // ... ) }
export function Dialog() { // initial focused element signal const [inputEl, setInputEl] = createSignal() const service = useMachine(dialog.machine, { initialFocusEl: inputEl, }) // ... return ( //... <input ref={setInputEl} /> // ... ) }
<script setup> import { ref } from "vue" // initial focused element ref const inputRef = ref(null) const service = useMachine(dialog.machine, { initialFocusEl: () => inputRef.value, }) </script> <template> <input ref="inputRef" /> </template>
<script lang="ts"> // initial focused element ref let inputRef: HTMLInputElement | null = null const service = useMachine( dialog.machine, ({ initialFocusEl: () => inputRef, }), ) // ... </script> <!-- ... --> <input bind:this={inputRef} /> <!-- ... -->
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 service = useMachine(dialog.machine, { closeOnInteractOutside: false, })
You can also customize the behavior by passing a function to the
onInteractOutside context and calling event.preventDefault()
const service = 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 service = useMachine(dialog.machine, { onOpenChange(details) { // details => { open: boolean } console.log("open:", details.open) }, })
Controlled dialog
To control the dialog's open state, pass the open and onOpenChange
properties.
import { useState } from "react" export function ControlledDialog() { const [open, setOpen] = useState(false) const service = useMachine(dialog.machine, { open, onOpenChange(details) { setOpen(details.open) }, }) return ( // ... ) }
import { createSignal } from "solid-js" export function ControlledDialog() { const [open, setOpen] = createSignal(false) const service = useMachine(dialog.machine, { get open() { return open() }, onOpenChange(details) { setOpen(details.open) }, }) return ( // ... ) }
<script lang="ts" setup> import { ref } from "vue" const openRef = ref(false) const service = useMachine(dialog.machine, { get open() { return openRef.value }, onOpenChange(details) { openRef.value = details.open }, }) </script>
<script lang="ts"> let open = $state(false) const service = useMachine(dialog.machine, { get open() { return open }, onOpenChange(details) { open = details.open }, }) </script>
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 service = 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 service = 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:
idsPartial<{ 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.trapFocusbooleanWhether to trap focus inside the dialog when it's openedpreventScrollbooleanWhether to prevent scrolling behind the dialog when it's openedmodalbooleanWhether to prevent pointer interaction outside the element and hide all content below itinitialFocusEl() => HTMLElementElement to receive focus when the dialog is openedfinalFocusEl() => HTMLElementElement to receive focus when the dialog is closedrestoreFocusbooleanWhether to restore focus to the element that had focus before the dialog was openedcloseOnInteractOutsidebooleanWhether to close the dialog when the outside is clickedcloseOnEscapebooleanWhether to close the dialog when the escape key is pressedaria-labelstringHuman readable label for the dialog, in event the dialog title is not renderedrole"dialog" | "alertdialog"The dialog's roleopenbooleanThe controlled open state of the dialogdefaultOpenbooleanThe initial open state of the dialog when rendered. Use when you don't need to control the open state of the dialog.onOpenChange(details: OpenChangeDetails) => voidFunction to call when the dialog's open state changesdir"ltr" | "rtl"The document's text/writing direction.idstringThe unique identifier of the machine.getRootNode() => Node | ShadowRoot | DocumentA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.onEscapeKeyDown(event: KeyboardEvent) => voidFunction called when the escape key is pressedonPointerDownOutside(event: PointerDownOutsideEvent) => voidFunction called when the pointer is pressed down outside the componentonFocusOutside(event: FocusOutsideEvent) => voidFunction called when the focus is moved outside the componentonInteractOutside(event: InteractOutsideEvent) => voidFunction called when an interaction happens outside the componentpersistentElements(() => Element)[]Returns the persistent elements that: - should not have pointer-events disabled - should not trigger the dismiss event
Machine API
The dialog api exposes the following methods:
openbooleanWhether the dialog is opensetOpen(open: boolean) => voidFunction to open or close the dialog
Data Attributes
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