{"slug":"dialog","title":"Dialog","description":"Using the dialog machine in your project.","contentType":"component","framework":"svelte","content":"A dialog is a window overlaid on either the primary window or another dialog\nwindow. Content behind a modal dialog is inert, meaning that users cannot\ninteract with it.\n\n## Resources\n\n\n[Latest version: v1.39.1](https://www.npmjs.com/package/@zag-js/dialog)\n[Logic Visualizer](https://zag-visualizer.vercel.app/dialog)\n[Source Code](https://github.com/chakra-ui/zag/tree/main/packages/machines/dialog)\n\n\n\n**Features**\n\n- Supports modal and non-modal modes\n- Focus is trapped and scrolling is blocked in the modal mode\n- Provides screen reader announcements via rendered title and description\n- Pressing `Esc` closes the dialog\n- Supports multiple triggers sharing a single dialog instance\n\n## Installation\n\nInstall the dialog package:\n\n```bash\nnpm install @zag-js/dialog @zag-js/svelte\n# or\nyarn add @zag-js/dialog @zag-js/svelte\n```\n\n## Anatomy\n\nTo use the dialog component correctly, you'll need to understand its anatomy and\nhow we name its parts.\n\n> Each part includes a `data-part` attribute to help identify them in the DOM.\n\n\n\n## Usage\n\nImport the dialog package:\n\n```tsx\nimport * as dialog from \"@zag-js/dialog\"\n```\n\nThe dialog package exports two key functions:\n\n- `machine` - Behavior logic for the dialog.\n- `connect` - Maps behavior to JSX props and event handlers.\n\n> Pass a unique `id` to `useMachine` so generated element ids stay predictable.\n\nThen use the framework integration helpers:\n\n```svelte\n<script lang=\"ts\">\n  import * as dialog from \"@zag-js/dialog\"\n  import { portal, normalizeProps, useMachine } from \"@zag-js/svelte\"\n\n  const id = $props.id()\n  const service = useMachine(dialog.machine, ({ id }))\n  const api = $derived(dialog.connect(service, normalizeProps))\n</script>\n\n<button {...api.getTriggerProps()}>Open Dialog</button>\n{#if api.open}\n  <div use:portal {...api.getBackdropProps()}></div>\n  <div use:portal {...api.getPositionerProps()}>\n    <div {...api.getContentProps()}>\n      <h2 {...api.getTitleProps()}>Edit profile</h2>\n      <p {...api.getDescriptionProps()}>Make changes to your profile here. Click save when you are done.</p>\n      <div>\n        <input placeholder=\"Enter name...\" />\n        <button>Save</button>\n      </div>\n      <button {...api.getCloseTriggerProps()}>Close</button>\n    </div>\n  </div>\n{/if}\n```\n\n### Managing focus within the dialog\n\nWhen the dialog opens, it focuses the first focusable element and keeps keyboard\nfocus inside the dialog.\n\nTo control what receives focus on open, pass `initialFocusEl`.\n\n```svelte {3,7,14}\n<script lang=\"ts\">\n  // initial focused element ref\n  let inputRef: HTMLInputElement | null = null\n\n  const service = useMachine(\n    dialog.machine, ({\n      initialFocusEl: () => inputRef,\n    }),\n  )\n  // ...\n</script>\n\n<!-- ... -->\n<input bind:this={inputRef} />\n<!-- ... -->\n```\n\nTo control what receives focus when the dialog closes, pass `finalFocusEl`.\n\n### Dialog vs non-modal dialog\n\nSet `modal` to `false` to allow interaction with content behind the dialog.\n\n```tsx\nconst service = useMachine(dialog.machine, {\n  modal: false,\n})\n```\n\n### Closing the dialog on interaction outside\n\nBy default, the dialog closes when you click its overlay. You can set\n`closeOnInteractOutside` to `false` if you want the modal to stay visible.\n\n```tsx {2}\nconst service = useMachine(dialog.machine, {\n  closeOnInteractOutside: false,\n})\n```\n\nYou can also customize the behavior by passing a function to the\n`onInteractOutside` callback and calling `event.preventDefault()`.\n\n```tsx {2-7}\nconst service = useMachine(dialog.machine, {\n  onInteractOutside(event) {\n    const target = event.target\n    if (target?.closest(\"<selector>\")) {\n      return event.preventDefault()\n    }\n  },\n})\n```\n\n### Listening for open state changes\n\nWhen the dialog is opened or closed, the `onOpenChange` callback is invoked.\n\n```tsx {2-7}\nconst service = useMachine(dialog.machine, {\n  onOpenChange(details) {\n    // details => { open: boolean }\n    console.log(\"open:\", details.open)\n  },\n})\n```\n\n### Closing with Escape\n\nSet `closeOnEscape` to `false` if the dialog should not close on `Esc`.\n\n```tsx\nconst service = useMachine(dialog.machine, {\n  closeOnEscape: false,\n})\n```\n\n### Controlled dialog\n\nTo control the dialog's open state, pass the `open` and `onOpenChange`\nproperties.\n\n```svelte\n<script lang=\"ts\">\n  let open = $state(false)\n\n  const service = useMachine(dialog.machine, {\n    get open() {\n      return open\n    },\n    onOpenChange(details) {\n      open = details.open\n    },\n  })\n</script>\n```\n\n### Controlling the scroll behavior\n\nWhen the dialog is open, it prevents scrolling on the `body` element. To disable\nthis behavior, set `preventScroll` to `false`.\n\n```tsx {2}\nconst service = useMachine(dialog.machine, {\n  preventScroll: false,\n})\n```\n\n### Creating an alert dialog\n\nThe dialog supports both `dialog` and `alertdialog` roles. It uses `dialog` by\ndefault. Set `role` to `alertdialog` for urgent actions.\n\nThat's it! Now you have an alert dialog.\n\n```tsx {2}\nconst service = useMachine(dialog.machine, {\n  role: \"alertdialog\",\n})\n```\n\n> By definition, an alert dialog will contain two or more action buttons. We\n> recommend setting focus to the least destructive action via `initialFocusEl`.\n\n### Multiple triggers\n\nA single dialog instance can be shared across multiple trigger elements. Pass a\n`value` to `getTriggerProps` to identify each trigger.\n\n```tsx\nconst users = [\n  { id: \"1\", name: \"Alice Johnson\" },\n  { id: \"2\", name: \"Bob Smith\" },\n]\n\nconst service = useMachine(dialog.machine, {\n  onTriggerValueChange({ value }) {\n    // value is the id of the trigger that activated the dialog\n    const user = users.find((u) => u.id === value) ?? null\n    setActiveUser(user)\n  },\n})\n\nconst api = dialog.connect(service, normalizeProps)\n\nreturn (\n  <>\n    {users.map((user) => (\n      <button {...api.getTriggerProps({ value: user.id })}>\n        Edit {user.name}\n      </button>\n    ))}\n    {/* single shared dialog */}\n    <div {...api.getBackdropProps()} />\n    <div {...api.getPositionerProps()}>\n      <div {...api.getContentProps()}>{/* ... */}</div>\n    </div>\n  </>\n)\n```\n\nWhen the dialog is open and a different trigger is activated, it switches without\nclosing. `aria-expanded` is scoped to the active trigger, and focus returns to\nthe correct trigger on close.\n\nYou can also read or set the active trigger programmatically:\n\n```tsx\n// Read the active trigger value\napi.triggerValue // => string | null\n\n// Set the active trigger value\napi.setTriggerValue(\"2\")\n```\n\n### Labeling without a visible title\n\nIf you do not render a title element, provide `aria-label`.\n\n```tsx\nconst service = useMachine(dialog.machine, {\n  \"aria-label\": \"Delete project\",\n})\n```\n\n## Styling guide\n\nEach part includes a `data-part` attribute you can target in CSS.\n\n```css\n[data-part=\"trigger\"] {\n  /* styles for the trigger element */\n}\n\n[data-part=\"backdrop\"] {\n  /* styles for the backdrop element */\n}\n\n[data-part=\"positioner\"] {\n  /* styles for the positioner element */\n}\n\n[data-part=\"content\"] {\n  /* styles for the content element */\n}\n\n[data-part=\"title\"] {\n  /* styles for the title element */\n}\n\n[data-part=\"description\"] {\n  /* styles for the description element */\n}\n\n[data-part=\"close-trigger\"] {\n  /* styles for the close trigger element */\n}\n```\n\n### Open and closed state\n\nThe dialog has two states: `open` and `closed`. You can use the `data-state`\nattribute to style the dialog or trigger based on its state.\n\n```css\n[data-part=\"content\"][data-state=\"open|closed\"] {\n  /* styles for the open state */\n}\n\n[data-part=\"trigger\"][data-state=\"open|closed\"] {\n  /* styles for the open state */\n}\n```\n\n### Nested dialogs\n\nWhen dialogs are nested (a dialog opened from within another dialog), the layer\nstack automatically applies data attributes to help create visual hierarchy.\n\n- `data-nested` - Applied to nested dialogs\n- `data-has-nested` - Applied to dialogs that have nested dialogs open\n- `--nested-layer-count` - CSS variable indicating the number of nested dialogs\n\n```css\n/* Scale down parent dialogs when they have nested children */\n[data-part=\"content\"][data-has-nested] {\n  transform: scale(calc(1 - var(--nested-layer-count) * 0.05));\n  transition: transform 0.2s ease-in-out;\n}\n\n/* Style nested dialogs differently */\n[data-part=\"content\"][data-nested] {\n  border: 2px solid var(--accent-color);\n}\n\n/* Create depth effect using backdrop opacity */\n[data-part=\"backdrop\"][data-has-nested] {\n  opacity: calc(0.4 + var(--nested-layer-count) * 0.1);\n}\n```\n\n## Methods and Properties\n\n### Machine Context\n\nThe dialog machine exposes the following context properties:\n\n**`ids`**\nType: `Partial<{ trigger: string | ((value?: string) => string); positioner: string; backdrop: string; content: string; closeTrigger: string; title: string; description: string; }>`\nDescription: The ids of the elements in the dialog. Useful for composition.\n\n**`trapFocus`**\nType: `boolean`\nDescription: Whether to trap focus inside the dialog when it's opened\n\n**`preventScroll`**\nType: `boolean`\nDescription: Whether to prevent scrolling behind the dialog when it's opened\n\n**`modal`**\nType: `boolean`\nDescription: Whether to prevent pointer interaction outside the element and hide all content below it\n\n**`initialFocusEl`**\nType: `() => HTMLElement`\nDescription: Element to receive focus when the dialog is opened\n\n**`finalFocusEl`**\nType: `() => HTMLElement`\nDescription: Element to receive focus when the dialog is closed\n\n**`restoreFocus`**\nType: `boolean`\nDescription: Whether to restore focus to the element that had focus before the dialog was opened\n\n**`closeOnInteractOutside`**\nType: `boolean`\nDescription: Whether to close the dialog when the outside is clicked\n\n**`closeOnEscape`**\nType: `boolean`\nDescription: Whether to close the dialog when the escape key is pressed\n\n**`aria-label`**\nType: `string`\nDescription: Human readable label for the dialog, in event the dialog title is not rendered\n\n**`role`**\nType: `\"dialog\" | \"alertdialog\"`\nDescription: The dialog's role\n\n**`open`**\nType: `boolean`\nDescription: The controlled open state of the dialog\n\n**`defaultOpen`**\nType: `boolean`\nDescription: The initial open state of the dialog when rendered.\nUse when you don't need to control the open state of the dialog.\n\n**`onOpenChange`**\nType: `(details: OpenChangeDetails) => void`\nDescription: Function to call when the dialog's open state changes\n\n**`triggerValue`**\nType: `string`\nDescription: The controlled active trigger value\n\n**`defaultTriggerValue`**\nType: `string`\nDescription: The initial active trigger value when rendered.\nUse when you don't need to control the active trigger value.\n\n**`onTriggerValueChange`**\nType: `(details: TriggerValueChangeDetails) => void`\nDescription: Function to call when the active trigger changes\n\n**`dir`**\nType: `\"ltr\" | \"rtl\"`\nDescription: The document's text/writing direction.\n\n**`id`**\nType: `string`\nDescription: The unique identifier of the machine.\n\n**`getRootNode`**\nType: `() => Node | ShadowRoot | Document`\nDescription: A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.\n\n**`onEscapeKeyDown`**\nType: `(event: KeyboardEvent) => void`\nDescription: Function called when the escape key is pressed\n\n**`onRequestDismiss`**\nType: `(event: LayerDismissEvent) => void`\nDescription: Function called when this layer is closed due to a parent layer being closed\n\n**`onPointerDownOutside`**\nType: `(event: PointerDownOutsideEvent) => void`\nDescription: Function called when the pointer is pressed down outside the component\n\n**`onFocusOutside`**\nType: `(event: FocusOutsideEvent) => void`\nDescription: Function called when the focus is moved outside the component\n\n**`onInteractOutside`**\nType: `(event: InteractOutsideEvent) => void`\nDescription: Function called when an interaction happens outside the component\n\n**`persistentElements`**\nType: `(() => Element)[]`\nDescription: Returns the persistent elements that:\n- should not have pointer-events disabled\n- should not trigger the dismiss event\n\n### Machine API\n\nThe dialog `api` exposes the following methods:\n\n**`open`**\nType: `boolean`\nDescription: Whether the dialog is open\n\n**`setOpen`**\nType: `(open: boolean) => void`\nDescription: Function to open or close the dialog\n\n**`triggerValue`**\nType: `string`\nDescription: The active trigger value\n\n**`setTriggerValue`**\nType: `(value: string) => void`\nDescription: Function to set the active trigger value\n\n### Data Attributes\n\n**`Trigger`**\n\n**`data-scope`**: dialog\n**`data-part`**: trigger\n**`data-value`**: The value of the item\n**`data-state`**: \"open\" | \"closed\"\n**`data-current`**: Present when current\n\n**`Backdrop`**\n\n**`data-scope`**: dialog\n**`data-part`**: backdrop\n**`data-state`**: \"open\" | \"closed\"\n\n**`Content`**\n\n**`data-scope`**: dialog\n**`data-part`**: content\n**`data-state`**: \"open\" | \"closed\"\n**`data-nested`**: dialog\n**`data-has-nested`**: dialog\n\n### CSS Variables\n\n<CssVarTable name=\"dialog\" />\n\n## Accessibility\n\nAdheres to the\n[Alert and Message Dialogs WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog).\n\n### Keyboard Interactions\n\n**`Enter`**\nDescription: When focus is on the trigger, opens the dialog.\n\n**`Tab`**\nDescription: Moves focus to the next focusable element within the content. Focus is trapped within the dialog.\n\n**`Shift + Tab`**\nDescription: Moves focus to the previous focusable element. Focus is trapped within the dialog.\n\n**`Esc`**\nDescription: Closes the dialog and moves focus to trigger or the defined final focus element","package":"@zag-js/dialog","editUrl":"https://github.com/chakra-ui/zag/edit/main/website/data/components/dialog.mdx"}