Dialog
A modal window that appears on top of the main content.
You can explore the dialog component in the following curated examples.
Command Menu
Render a Command Palette that opens when CMD + K is pressed.
Dialog with Tooltip
Render a Dialog with a Tooltip that shows upon hovering over the Dialog Trigger.
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.
Examples
Learn how to use the Dialog
component in your project. Let's take a look at the most basic example
import { Dialog } from '@ark-ui/react/dialog'
import { Portal } from '@ark-ui/react/portal'
import { XIcon } from 'lucide-react'
export const Basic = () => (
<Dialog.Root>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
)
import { XIcon } from 'lucide-solid'
import { Dialog } from '@ark-ui/solid/dialog'
import { Portal } from 'solid-js/web'
export const Basic = () => {
return (
<Dialog.Root defaultOpen>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
)
}
<script setup lang="ts">
import { Dialog } from '@ark-ui/vue/dialog'
import { XIcon } from 'lucide-vue-next'
</script>
<template>
<Dialog.Root>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Teleport to="body">
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Teleport>
</Dialog.Root>
</template>
<script lang="ts">
import { Dialog } from '@ark-ui/svelte/dialog'
import { XIcon } from 'lucide-svelte'
import { Portal } from '@ark-ui/svelte/portal'
</script>
<Dialog.Root>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
Controlled
To create a controlled Dialog component, manage the state of the dialog using the open
and onOpenChange
props:
import { Dialog } from '@ark-ui/react/dialog'
import { Portal } from '@ark-ui/react/portal'
import { XIcon } from 'lucide-react'
import { useState } from 'react'
export const Controlled = () => {
const [isOpen, setIsOpen] = useState(false)
return (
<>
<button type="button" onClick={() => setIsOpen(true)}>
Open Dialog
</button>
<Dialog.Root open={isOpen} onOpenChange={(e) => setIsOpen(e.open)}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
</>
)
}
import { XIcon } from 'lucide-solid'
import { Dialog } from '@ark-ui/solid/dialog'
import { createSignal } from 'solid-js'
import { Portal } from 'solid-js/web'
export const Controlled = () => {
const [open, setOpen] = createSignal(false)
return (
<>
<button type="button" onClick={() => setOpen(true)}>
Open Dialog
</button>
<Dialog.Root open={open()} onOpenChange={() => setOpen(false)}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
</>
)
}
<script setup lang="ts">
import { Dialog } from '@ark-ui/vue/dialog'
import { XIcon } from 'lucide-vue-next'
import { ref } from 'vue'
const open = ref(false)
</script>
<template>
<button @click="() => (open = true)">{{ open ? 'Close' : 'Open' }} Dialog</button>
<Dialog.Root v-model:open="open">
<Teleport to="body">
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Teleport>
</Dialog.Root>
</template>
<script lang="ts">
import { Dialog } from '@ark-ui/svelte/dialog'
import { XIcon } from 'lucide-svelte'
import { Portal } from '@ark-ui/svelte/portal'
let open = $state(false)
</script>
<button type="button" onclick={() => (open = !open)}>Toggle</button>
<Dialog.Root bind:open>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Controlled Dialog</Dialog.Title>
<Dialog.Description>This dialog's open state is controlled externally.</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
Lazy Mount
Lazy mounting is a feature that allows the content of a dialog to be rendered only when the dialog is first opened. This
is useful for performance optimization, especially when dialog content is large or complex. To enable lazy mounting, use
the lazyMount
prop on the Dialog.Root
component.
In addition, the unmountOnExit
prop can be used in conjunction with lazyMount
to unmount the dialog content when the
Dialog is closed, freeing up resources. The next time the dialog is activated, its content will be re-rendered.
import { Dialog } from '@ark-ui/react/dialog'
import { Portal } from '@ark-ui/react/portal'
import { XIcon } from 'lucide-react'
export const LazyMount = () => (
<Dialog.Root lazyMount unmountOnExit onExitComplete={() => console.log('onExitComplete invoked')}>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
)
import { XIcon } from 'lucide-solid'
import { Dialog } from '@ark-ui/solid/dialog'
export const LazyMount = () => {
return (
<Dialog.Root lazyMount unmountOnExit>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Dialog.Root>
)
}
<script setup lang="ts">
import { Dialog } from '@ark-ui/vue/dialog'
import { XIcon } from 'lucide-vue-next'
</script>
<template>
<Dialog.Root lazyMount unmountOnExit>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Dialog.Root>
</template>
<script lang="ts">
import { Dialog } from '@ark-ui/svelte/dialog'
import { XIcon } from 'lucide-svelte'
import { Portal } from '@ark-ui/svelte/portal'
</script>
<Dialog.Root lazyMount unmountOnExit onExitComplete={() => console.log('onExitComplete invoked')}>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Lazy Mounted Dialog</Dialog.Title>
<Dialog.Description>This dialog content is only rendered when the dialog is first opened.</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
Alert Dialog
For critical confirmations or destructive actions, use role="alertdialog"
. Alert dialogs differ from regular dialogs
in important ways:
- Automatic focus: The close/cancel button receives focus when opened, prioritizing the safest action
- Requires explicit dismissal: Cannot be closed by clicking outside, only via button clicks or Escape key
import { Dialog } from '@ark-ui/react/dialog'
import { Portal } from '@ark-ui/react/portal'
import { XIcon } from 'lucide-react'
export const AlertDialog = () => (
<Dialog.Root role="alertdialog">
<Dialog.Trigger>Delete Account</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Are you absolutely sure?</Dialog.Title>
<Dialog.Description>
This action cannot be undone. This will permanently delete your account and remove your data from our
servers.
</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
<button type="button">Delete Account</button>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
)
import { XIcon } from 'lucide-solid'
import { Dialog } from '@ark-ui/solid/dialog'
import { Portal } from 'solid-js/web'
export const AlertDialog = () => {
return (
<Dialog.Root role="alertdialog">
<Dialog.Trigger>Delete Account</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Are you absolutely sure?</Dialog.Title>
<Dialog.Description>
This action cannot be undone. This will permanently delete your account and remove your data from our
servers.
</Dialog.Description>
<div>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
<button type="button">Delete Account</button>
</div>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
)
}
<script setup lang="ts">
import { Dialog } from '@ark-ui/vue/dialog'
import { XIcon } from 'lucide-vue-next'
</script>
<template>
<Dialog.Root role="alertdialog">
<Dialog.Trigger>Delete Account</Dialog.Trigger>
<Teleport to="body">
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Are you absolutely sure?</Dialog.Title>
<Dialog.Description>
This action cannot be undone. This will permanently delete your account and remove your data from our
servers.
</Dialog.Description>
<div>
<Dialog.CloseTrigger>Cancel</Dialog.CloseTrigger>
<button type="button">Delete Account</button>
</div>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Teleport>
</Dialog.Root>
</template>
<script lang="ts">
import { Dialog } from '@ark-ui/svelte/dialog'
import { XIcon } from 'lucide-svelte'
import { Portal } from '@ark-ui/svelte/portal'
</script>
<Dialog.Root role="alertdialog">
<Dialog.Trigger>Delete Account</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Are you absolutely sure?</Dialog.Title>
<Dialog.Description>
This action cannot be undone. This will permanently delete your account and remove your data from our servers.
</Dialog.Description>
<div>
<Dialog.CloseTrigger>Cancel</Dialog.CloseTrigger>
<button type="button">Delete Account</button>
</div>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
Initial Focus
Control which element receives focus when the dialog opens using the initialFocusEl
prop. This is useful for forms
where you want to focus a specific input field:
import { Dialog } from '@ark-ui/react/dialog'
import { Portal } from '@ark-ui/react/portal'
import { XIcon } from 'lucide-react'
import { useRef } from 'react'
export const InitialFocus = () => {
const inputRef = useRef<HTMLInputElement>(null)
return (
<Dialog.Root initialFocusEl={() => inputRef.current}>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Edit Profile</Dialog.Title>
<Dialog.Description>
Make changes to your profile here. The first input will be focused when the dialog opens.
</Dialog.Description>
<input ref={inputRef} type="text" placeholder="Enter your name..." />
<input type="email" placeholder="Enter your email..." />
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
)
}
import { XIcon } from 'lucide-solid'
import { Dialog } from '@ark-ui/solid/dialog'
import { Portal } from 'solid-js/web'
export const InitialFocus = () => {
let inputRef: HTMLInputElement | null = null
return (
<Dialog.Root initialFocusEl={() => inputRef}>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Edit Profile</Dialog.Title>
<Dialog.Description>
Make changes to your profile here. The first input will be focused when the dialog opens.
</Dialog.Description>
<input ref={inputRef!} type="text" placeholder="Enter your name..." />
<input type="email" placeholder="Enter your email..." />
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
)
}
<script setup lang="ts">
import { Dialog } from '@ark-ui/vue/dialog'
import { XIcon } from 'lucide-vue-next'
import { ref } from 'vue'
const inputRef = ref<HTMLInputElement | null>(null)
</script>
<template>
<Dialog.Root :initial-focus-el="() => inputRef">
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Teleport to="body">
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Edit Profile</Dialog.Title>
<Dialog.Description>
Make changes to your profile here. The first input will be focused when the dialog opens.
</Dialog.Description>
<input ref="inputRef" type="text" placeholder="Enter your name..." />
<input type="email" placeholder="Enter your email..." />
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Teleport>
</Dialog.Root>
</template>
<script lang="ts">
import { Dialog } from '@ark-ui/svelte/dialog'
import { XIcon } from 'lucide-svelte'
import { Portal } from '@ark-ui/svelte/portal'
let inputRef: HTMLInputElement
</script>
<Dialog.Root initialFocusEl={() => inputRef}>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Edit Profile</Dialog.Title>
<Dialog.Description>
Make changes to your profile here. The first input will be focused when the dialog opens.
</Dialog.Description>
<input bind:this={inputRef} type="text" placeholder="Enter your name..." />
<input type="email" placeholder="Enter your email..." />
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
Final Focus
Control which element receives focus when the dialog closes using the finalFocusEl
prop. By default, focus returns to
the trigger element, but you can specify a different element:
import { Dialog } from '@ark-ui/react/dialog'
import { Portal } from '@ark-ui/react/portal'
import { XIcon } from 'lucide-react'
import { useRef } from 'react'
export const FinalFocus = () => {
const finalRef = useRef<HTMLButtonElement>(null)
return (
<>
<button type="button" ref={finalRef}>
I will receive focus when dialog closes
</button>
<Dialog.Root finalFocusEl={() => finalRef.current}>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>
When this dialog closes, focus will return to the button above instead of the trigger.
</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
</>
)
}
import { XIcon } from 'lucide-solid'
import { Dialog } from '@ark-ui/solid/dialog'
import { Portal } from 'solid-js/web'
export const FinalFocus = () => {
let finalRef: HTMLButtonElement | null = null
return (
<>
<button type="button" ref={finalRef!}>
I will receive focus when dialog closes
</button>
<Dialog.Root finalFocusEl={() => finalRef}>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>
When this dialog closes, focus will return to the button above instead of the trigger.
</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
</>
)
}
<script setup lang="ts">
import { Dialog } from '@ark-ui/vue/dialog'
import { XIcon } from 'lucide-vue-next'
import { ref } from 'vue'
const finalRef = ref<HTMLButtonElement | null>(null)
</script>
<template>
<button type="button" ref="finalRef">I will receive focus when dialog closes</button>
<Dialog.Root :final-focus-el="() => finalRef">
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Teleport to="body">
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>
When this dialog closes, focus will return to the button above instead of the trigger.
</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Teleport>
</Dialog.Root>
</template>
<script lang="ts">
import { Dialog } from '@ark-ui/svelte/dialog'
import { XIcon } from 'lucide-svelte'
import { Portal } from '@ark-ui/svelte/portal'
let finalRef: HTMLButtonElement
</script>
<button type="button" bind:this={finalRef}>I will receive focus when dialog closes</button>
<Dialog.Root finalFocusEl={() => finalRef}>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>
When this dialog closes, focus will return to the button above instead of the trigger.
</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
Non-Modal Dialog
Use modal={false}
to create a non-modal dialog that allows interaction with elements outside of it. This disables
focus trapping, scroll prevention, and pointer blocking, making it useful for auxiliary panels or inspector windows:
import { Dialog } from '@ark-ui/react/dialog'
import { Portal } from '@ark-ui/react/portal'
import { XIcon } from 'lucide-react'
export const NonModal = () => (
<Dialog.Root modal={false}>
<Dialog.Trigger>Open Non-Modal Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Non-Modal Dialog</Dialog.Title>
<Dialog.Description>
This dialog allows interaction with elements outside. You can click buttons, select text, and interact with
the page behind it.
</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
)
import { XIcon } from 'lucide-solid'
import { Dialog } from '@ark-ui/solid/dialog'
import { Portal } from 'solid-js/web'
export const NonModal = () => {
return (
<Dialog.Root modal={false}>
<Dialog.Trigger>Open Non-Modal Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Non-Modal Dialog</Dialog.Title>
<Dialog.Description>
This dialog allows interaction with elements outside. You can click buttons, select text, and interact
with the page behind it.
</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
)
}
<script setup lang="ts">
import { Dialog } from '@ark-ui/vue/dialog'
import { XIcon } from 'lucide-vue-next'
</script>
<template>
<Dialog.Root :modal="false">
<Dialog.Trigger>Open Non-Modal Dialog</Dialog.Trigger>
<Teleport to="body">
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Non-Modal Dialog</Dialog.Title>
<Dialog.Description>
This dialog allows interaction with elements outside. You can click buttons, select text, and interact with
the page behind it.
</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Teleport>
</Dialog.Root>
</template>
<script lang="ts">
import { Dialog } from '@ark-ui/svelte/dialog'
import { XIcon } from 'lucide-svelte'
import { Portal } from '@ark-ui/svelte/portal'
</script>
<Dialog.Root modal={false}>
<Dialog.Trigger>Open Non-Modal Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Non-Modal Dialog</Dialog.Title>
<Dialog.Description>
This dialog allows interaction with elements outside. You can click buttons, select text, and interact with
the page behind it.
</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
Close on Interact Outside
Prevent the dialog from closing when clicking outside by setting closeOnInteractOutside={false}
. Use the
onInteractOutside
event with e.preventDefault()
for advanced control:
import { Dialog } from '@ark-ui/react/dialog'
import { Portal } from '@ark-ui/react/portal'
import { XIcon } from 'lucide-react'
export const CloseOnInteractOutside = () => (
<Dialog.Root
closeOnInteractOutside={false}
onInteractOutside={(e) => {
const target = e.target as HTMLElement
if (target.closest('[data-allow-close]')) {
return
}
e.preventDefault()
}}
>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Custom Close Behavior</Dialog.Title>
<Dialog.Description>
This dialog will not close when clicking outside. Try clicking the backdrop or pressing Escape to see that
it stays open. Only the close button will dismiss it.
</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
)
import { XIcon } from 'lucide-solid'
import { Dialog } from '@ark-ui/solid/dialog'
import { Portal } from 'solid-js/web'
export const CloseOnInteractOutside = () => {
return (
<Dialog.Root
closeOnInteractOutside={false}
onInteractOutside={(e) => {
const target = e.target as HTMLElement
if (target.closest('[data-allow-close]')) {
return
}
e.preventDefault()
}}
>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Custom Close Behavior</Dialog.Title>
<Dialog.Description>
This dialog will not close when clicking outside. Try clicking the backdrop or pressing Escape to see that
it stays open. Only the close button will dismiss it.
</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
)
}
<script setup lang="ts">
import { Dialog } from '@ark-ui/vue/dialog'
import { XIcon } from 'lucide-vue-next'
const handleInteractOutside = (e: CustomEvent) => {
const target = e.detail.target as HTMLElement
if (target.closest('[data-allow-close]')) {
return
}
e.preventDefault()
}
</script>
<template>
<Dialog.Root :close-on-interact-outside="false" :on-interact-outside="handleInteractOutside">
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Teleport to="body">
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Custom Close Behavior</Dialog.Title>
<Dialog.Description>
This dialog will not close when clicking outside. Try clicking the backdrop or pressing Escape to see that
it stays open. Only the close button will dismiss it.
</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Teleport>
</Dialog.Root>
</template>
<script lang="ts">
import { Dialog } from '@ark-ui/svelte/dialog'
import { XIcon } from 'lucide-svelte'
import { Portal } from '@ark-ui/svelte/portal'
const handleInteractOutside = (e: CustomEvent) => {
const target = e.detail.target as HTMLElement
if (target.closest('[data-allow-close]')) {
return
}
e.preventDefault()
}
</script>
<Dialog.Root closeOnInteractOutside={false} onInteractOutside={handleInteractOutside}>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Custom Close Behavior</Dialog.Title>
<Dialog.Description>
This dialog will not close when clicking outside. Try clicking the backdrop or pressing
Escape to see that it stays open. Only the close button will dismiss it.
</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
Close on Escape
Prevent the dialog from closing when pressing Escape by setting closeOnEscape={false}
. Use the onEscapeKeyDown
event
with e.preventDefault()
to implement custom behavior like unsaved changes warnings:
import { Dialog } from '@ark-ui/react/dialog'
import { Portal } from '@ark-ui/react/portal'
import { XIcon } from 'lucide-react'
export const CloseOnEscape = () => (
<Dialog.Root
closeOnEscape={false}
onEscapeKeyDown={(e) => {
const hasUnsavedChanges = true
if (hasUnsavedChanges) {
e.preventDefault()
alert('You have unsaved changes. Please save or discard them before closing.')
}
}}
>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Unsaved Changes</Dialog.Title>
<Dialog.Description>
This dialog prevents closing with Escape key. Try pressing Escape to see the warning. Use the close button
to dismiss.
</Dialog.Description>
<textarea placeholder="Type something..." rows={4} style={{ width: '100%' }} />
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
)
import { XIcon } from 'lucide-solid'
import { Dialog } from '@ark-ui/solid/dialog'
import { Portal } from 'solid-js/web'
export const CloseOnEscape = () => {
return (
<Dialog.Root
closeOnEscape={false}
onEscapeKeyDown={(e) => {
const hasUnsavedChanges = true
if (hasUnsavedChanges) {
e.preventDefault()
alert('You have unsaved changes. Please save or discard them before closing.')
}
}}
>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Unsaved Changes</Dialog.Title>
<Dialog.Description>
This dialog prevents closing with Escape key. Try pressing Escape to see the warning. Use the close button
to dismiss.
</Dialog.Description>
<textarea placeholder="Type something..." rows={4} style={{ width: '100%' }} />
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
)
}
<script setup lang="ts">
import { Dialog } from '@ark-ui/vue/dialog'
import { XIcon } from 'lucide-vue-next'
const handleEscapeKeyDown = (e: KeyboardEvent) => {
const hasUnsavedChanges = true
if (hasUnsavedChanges) {
e.preventDefault()
alert('You have unsaved changes. Please save or discard them before closing.')
}
}
</script>
<template>
<Dialog.Root :close-on-escape="false" :on-escape-key-down="handleEscapeKeyDown">
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Teleport to="body">
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Unsaved Changes</Dialog.Title>
<Dialog.Description>
This dialog prevents closing with Escape key. Try pressing Escape to see the warning. Use the close button
to dismiss.
</Dialog.Description>
<textarea placeholder="Type something..." rows="4" style="width: 100%" />
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Teleport>
</Dialog.Root>
</template>
<script lang="ts">
import { Dialog } from '@ark-ui/svelte/dialog'
import { XIcon } from 'lucide-svelte'
import { Portal } from '@ark-ui/svelte/portal'
const handleEscapeKeyDown = (e: KeyboardEvent) => {
const hasUnsavedChanges = true
if (hasUnsavedChanges) {
e.preventDefault()
alert('You have unsaved changes. Please save or discard them before closing.')
}
}
</script>
<Dialog.Root closeOnEscape={false} onEscapeKeyDown={handleEscapeKeyDown}>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Unsaved Changes</Dialog.Title>
<Dialog.Description>
This dialog prevents closing with Escape key. Try pressing Escape to see the warning. Use the close button to
dismiss.
</Dialog.Description>
<textarea placeholder="Type something..." rows={4} style="width: 100%;"></textarea>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
Render Function
Use the Dialog.Context
component to access the dialog's state and methods.
import { Dialog } from '@ark-ui/react/dialog'
import { Portal } from '@ark-ui/react/portal'
import { XIcon } from 'lucide-react'
export const RenderFn = () => (
<Dialog.Root>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
<Dialog.Context>{(dialog) => <p>Dialog is {dialog.open ? 'open' : 'closed'}</p>}</Dialog.Context>
</Dialog.Root>
)
import { XIcon } from 'lucide-solid'
import { Dialog } from '@ark-ui/solid/dialog'
import { Portal } from 'solid-js/web'
export const RenderFn = () => {
return (
<Dialog.Root>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
<Dialog.Context>{(context) => <p>Dialog is {context().open ? 'open' : 'closed'}</p>}</Dialog.Context>
</Dialog.Root>
)
}
<script setup lang="ts">
import { Dialog } from '@ark-ui/vue/dialog'
import { XIcon } from 'lucide-vue-next'
</script>
<template>
<Dialog.Root>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Teleport to="body">
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Teleport>
<Dialog.Context v-slot="dialog">
<p>Dialog is {{ dialog.open ? 'open' : 'closed' }}</p>
</Dialog.Context>
</Dialog.Root>
</template>
<script lang="ts">
import { Dialog } from '@ark-ui/svelte/dialog'
import { XIcon } from 'lucide-svelte'
import { Portal } from '@ark-ui/svelte/portal'
</script>
<Dialog.Root>
<Dialog.Context>
{#snippet children(dialog)}
<Dialog.Trigger>{dialog().open ? 'Close' : 'Open'} Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog State: {dialog().open ? 'Open' : 'Closed'}</Dialog.Title>
<Dialog.Description>This example uses a render function to access dialog state.</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
{/snippet}
</Dialog.Context>
</Dialog.Root>
Root Provider
The useDialog
hook gives you programmatic access to the dialog's state and methods. Use it with Dialog.RootProvider
when you need to control the dialog from outside its component tree.
import { Dialog, useDialog } from '@ark-ui/react/dialog'
import { Portal } from '@ark-ui/react/portal'
import { XIcon } from 'lucide-react'
export const RootProvider = () => {
const dialog = useDialog()
return (
<>
<button onClick={() => dialog.setOpen(true)}>Open</button>
<Dialog.RootProvider value={dialog}>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.RootProvider>
</>
)
}
import { XIcon } from 'lucide-solid'
import { Dialog, useDialog } from '@ark-ui/solid/dialog'
import { Portal } from 'solid-js/web'
export const RootProvider = () => {
const dialog = useDialog()
return (
<>
<button onClick={() => dialog().setOpen(true)}>Open</button>
<Dialog.RootProvider value={dialog}>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.RootProvider>
</>
)
}
<script setup lang="ts">
import { Dialog, useDialog } from '@ark-ui/vue/dialog'
const dialog = useDialog()
</script>
<template>
<button @click="dialog.setOpen(true)">Open</button>
<Dialog.RootProvider :value="dialog">
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Teleport to="body">
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Teleport>
</Dialog.RootProvider>
</template>
<script lang="ts">
import { Dialog, useDialog } from '@ark-ui/svelte/dialog'
import { XIcon } from 'lucide-svelte'
import { Portal } from '@ark-ui/svelte/portal'
const id = $props.id()
const dialog = useDialog({ id })
</script>
<button onclick={() => dialog().setOpen(true)}>Open</button>
<Dialog.RootProvider value={dialog}>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.RootProvider>
Note: There are two ways to use the Dialog component: (1)
Dialog.Root
for declarative usage, or (2)useDialog()
+Dialog.RootProvider
for programmatic control with access to state properties and methods likesetOpen()
. Never use both approaches together - choose one based on your needs.
Nested Dialogs
Multiple dialogs can be stacked with automatic z-index management. Zag.js manages layering through CSS variables like
--z-index
and --layer-index
, which are automatically updated when dialogs are opened or closed:
import { Dialog, useDialog } from '@ark-ui/react/dialog'
import { Portal } from '@ark-ui/react/portal'
import { XIcon } from 'lucide-react'
export const Nested = () => {
const parentDialog = useDialog()
const childDialog = useDialog()
return (
<>
<button onClick={() => parentDialog.setOpen(true)}>Open Parent Dialog</button>
<Dialog.RootProvider value={parentDialog}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Parent Dialog</Dialog.Title>
<Dialog.Description>
This is the parent dialog. Open a nested dialog from here to see automatic z-index management via CSS
variables like --z-index and --layer-index.
</Dialog.Description>
<button onClick={() => childDialog.setOpen(true)}>Open Nested Dialog</button>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.RootProvider>
<Dialog.RootProvider value={childDialog}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Nested Dialog</Dialog.Title>
<Dialog.Description>
This dialog is nested within the parent. Notice how it layers on top with proper z-index management.
</Dialog.Description>
<button onClick={() => parentDialog.setOpen(false)}>Close Parent Dialog</button>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.RootProvider>
</>
)
}
import { XIcon } from 'lucide-solid'
import { Dialog, useDialog } from '@ark-ui/solid/dialog'
import { Portal } from 'solid-js/web'
export const Nested = () => {
const parentDialog = useDialog()
const childDialog = useDialog()
return (
<>
<button onClick={() => parentDialog().setOpen(true)}>Open Parent Dialog</button>
<Dialog.RootProvider value={parentDialog}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Parent Dialog</Dialog.Title>
<Dialog.Description>
This is the parent dialog. Open a nested dialog from here to see automatic z-index management via CSS
variables like --z-index and --layer-index.
</Dialog.Description>
<button onClick={() => childDialog().setOpen(true)}>Open Nested Dialog</button>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.RootProvider>
<Dialog.RootProvider value={childDialog}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Nested Dialog</Dialog.Title>
<Dialog.Description>
This dialog is nested within the parent. Notice how it layers on top with proper z-index management.
</Dialog.Description>
<button onClick={() => parentDialog().setOpen(false)}>Close Parent Dialog</button>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.RootProvider>
</>
)
}
<script setup lang="ts">
import { Dialog, useDialog } from '@ark-ui/vue/dialog'
const parentDialog = useDialog()
const childDialog = useDialog()
</script>
<template>
<button @click="parentDialog.setOpen(true)">Open Parent Dialog</button>
<Dialog.RootProvider :value="parentDialog">
<Teleport to="body">
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Parent Dialog</Dialog.Title>
<Dialog.Description>
This is the parent dialog. Open a nested dialog from here to see automatic z-index management via CSS
variables like --z-index and --layer-index.
</Dialog.Description>
<button @click="childDialog.setOpen(true)">Open Nested Dialog</button>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Teleport>
</Dialog.RootProvider>
<Dialog.RootProvider :value="childDialog">
<Teleport to="body">
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Nested Dialog</Dialog.Title>
<Dialog.Description>
This dialog is nested within the parent. Notice how it layers on top with proper z-index management.
</Dialog.Description>
<button @click="parentDialog.setOpen(false)">Close Parent Dialog</button>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Teleport>
</Dialog.RootProvider>
</template>
<script lang="ts">
import { Dialog, useDialog } from '@ark-ui/svelte/dialog'
import { XIcon } from 'lucide-svelte'
import { Portal } from '@ark-ui/svelte/portal'
const id = $props.id()
const parentDialog = useDialog({ id: `${id}-parent` })
const childDialog = useDialog({ id: `${id}-child` })
</script>
<button onclick={() => parentDialog().setOpen(true)}>Open Parent Dialog</button>
<Dialog.RootProvider value={parentDialog}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Parent Dialog</Dialog.Title>
<Dialog.Description>
This is the parent dialog. Open a nested dialog from here to see automatic z-index management via CSS
variables like `--z-index` and `--layer-index`.
</Dialog.Description>
<button onclick={() => childDialog().setOpen(true)}>Open Nested Dialog</button>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.RootProvider>
<Dialog.RootProvider value={childDialog}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Nested Dialog</Dialog.Title>
<Dialog.Description>
This dialog is nested within the parent. Notice how it layers on top with proper z-index management.
</Dialog.Description>
<button onclick={() => parentDialog().setOpen(false)}>Close Parent Dialog</button>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.RootProvider>
You can create a zoom-out effect for parent dialogs using the data-has-nested
attribute and --nested-layer-count
variable:
[data-scope='dialog'][data-part='backdrop'][data-has-nested] {
transform: scale(calc(1 - var(--nested-layer-count) * 0.05));
}
Confirmation Dialog
Dialogs can intercept close attempts to show confirmation prompts. This pattern is useful for preventing data loss from unsaved changes:
import { Dialog, useDialog } from '@ark-ui/react/dialog'
import { Portal } from '@ark-ui/react/portal'
import { XIcon } from 'lucide-react'
import { useState } from 'react'
export const Confirmation = () => {
const [formContent, setFormContent] = useState('')
const [isParentDialogOpen, setIsParentDialogOpen] = useState(false)
const parentDialog = useDialog({
open: isParentDialogOpen,
onOpenChange: (details) => {
if (!details.open && formContent.trim()) {
confirmDialog.setOpen(true)
} else {
setIsParentDialogOpen(details.open)
}
},
})
const confirmDialog = useDialog()
const handleConfirmClose = () => {
confirmDialog.setOpen(false)
parentDialog.setOpen(false)
setFormContent('')
}
return (
<>
<button onClick={() => parentDialog.setOpen(true)}>Open Form</button>
<Dialog.RootProvider value={parentDialog}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Edit Content</Dialog.Title>
<Dialog.Description>
Make changes to your content. If you have unsaved changes, you'll be asked to confirm before closing.
</Dialog.Description>
<textarea
value={formContent}
onChange={(e) => setFormContent(e.target.value)}
placeholder="Enter some text..."
rows={4}
style={{ width: '100%' }}
/>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.RootProvider>
<Dialog.RootProvider value={confirmDialog}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Unsaved Changes</Dialog.Title>
<Dialog.Description>
You have unsaved changes. Are you sure you want to close without saving?
</Dialog.Description>
<div>
<button onClick={() => confirmDialog.setOpen(false)}>Keep Editing</button>
<button onClick={handleConfirmClose}>Discard Changes</button>
</div>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.RootProvider>
</>
)
}
import { XIcon } from 'lucide-solid'
import { Dialog, useDialog } from '@ark-ui/solid/dialog'
import { createSignal } from 'solid-js'
import { Portal } from 'solid-js/web'
export const Confirmation = () => {
const [formContent, setFormContent] = createSignal('')
const parentDialog = useDialog({
onOpenChange: (details) => {
if (!details.open && formContent().trim()) {
confirmDialog().setOpen(true)
}
},
})
const confirmDialog = useDialog()
const handleConfirmClose = () => {
confirmDialog().setOpen(false)
parentDialog().setOpen(false)
setFormContent('')
}
return (
<>
<button onClick={() => parentDialog().setOpen(true)}>Open Form</button>
<Dialog.RootProvider value={parentDialog}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Edit Content</Dialog.Title>
<Dialog.Description>
Make changes to your content. If you have unsaved changes, you'll be asked to confirm before closing.
</Dialog.Description>
<textarea
value={formContent()}
onInput={(e) => setFormContent(e.currentTarget.value)}
placeholder="Enter some text..."
rows={4}
style={{ width: '100%' }}
/>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.RootProvider>
<Dialog.RootProvider value={confirmDialog}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Unsaved Changes</Dialog.Title>
<Dialog.Description>
You have unsaved changes. Are you sure you want to close without saving?
</Dialog.Description>
<div>
<button onClick={() => confirmDialog().setOpen(false)}>Keep Editing</button>
<button onClick={handleConfirmClose}>Discard Changes</button>
</div>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.RootProvider>
</>
)
}
<script setup lang="ts">
import { Dialog, useDialog } from '@ark-ui/vue/dialog'
import { XIcon } from 'lucide-vue-next'
import { ref } from 'vue'
const formContent = ref('')
const confirmDialog = useDialog()
const parentDialog = useDialog({
onOpenChange: (details) => {
if (!details.open && formContent.value.trim()) {
confirmDialog.value.setOpen(true)
}
},
})
const handleConfirmClose = () => {
confirmDialog.value.setOpen(false)
parentDialog.value.setOpen(false)
formContent.value = ''
}
</script>
<template>
<button @click="parentDialog.setOpen(true)">Open Form</button>
<Dialog.RootProvider :value="parentDialog">
<Teleport to="body">
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Edit Content</Dialog.Title>
<Dialog.Description>
Make changes to your content. If you have unsaved changes, you'll be asked to confirm before closing.
</Dialog.Description>
<textarea v-model="formContent" placeholder="Enter some text..." rows="4" style="width: 100%" />
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Teleport>
</Dialog.RootProvider>
<Dialog.RootProvider :value="confirmDialog">
<Teleport to="body">
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Unsaved Changes</Dialog.Title>
<Dialog.Description>
You have unsaved changes. Are you sure you want to close without saving?
</Dialog.Description>
<div>
<button @click="confirmDialog.setOpen(false)">Keep Editing</button>
<button @click="handleConfirmClose">Discard Changes</button>
</div>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Teleport>
</Dialog.RootProvider>
</template>
<script lang="ts">
import { Dialog, useDialog } from '@ark-ui/svelte/dialog'
import { XIcon } from 'lucide-svelte'
import { Portal } from '@ark-ui/svelte/portal'
const id = $props.id()
let formContent = $state('')
const confirmDialog = useDialog({ id: `${id}-confirm` })
const parentDialog = useDialog({
id: `${id}-parent`,
onOpenChange: (details) => {
if (!details.open && formContent.trim()) {
confirmDialog().setOpen(true)
}
},
})
const handleConfirmClose = () => {
confirmDialog().setOpen(false)
parentDialog().setOpen(false)
formContent = ''
}
</script>
<button onclick={() => parentDialog().setOpen(true)}>Open Form</button>
<Dialog.RootProvider value={parentDialog}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Edit Content</Dialog.Title>
<Dialog.Description>
Make changes to your content. If you have unsaved changes, you'll be asked to confirm before closing.
</Dialog.Description>
<textarea bind:value={formContent} placeholder="Enter some text..." rows={4} style="width: 100%;"></textarea>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.RootProvider>
<Dialog.RootProvider value={confirmDialog}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Unsaved Changes</Dialog.Title>
<Dialog.Description>
You have unsaved changes. Are you sure you want to close without saving?
</Dialog.Description>
<div>
<button onclick={() => confirmDialog().setOpen(false)}>Keep Editing</button>
<button onclick={handleConfirmClose}>Discard Changes</button>
</div>
<Dialog.CloseTrigger>
<XIcon />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.RootProvider>
Guides
Lazy Mount and Dynamic Imports
When using lazyMount
and dynamically rendering components in the dialog (via React.lazy
, Next.js dynamic
), wrap
the imported component in a Suspense
component to render a fallback.
import { Dialog } from '@ark-ui/react/dialog'
import { Suspense } from 'react'
import dynamic from 'next/dynamic'
const HeavyComponent = dynamic(() => import('./HeavyComponent'))
export default function DialogExample() {
return (
<Dialog.Root lazyMount>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Dialog.Content>
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
</Dialog.Content>
</Dialog.Root>
)
}
API Reference
Root
Prop | Default | Type |
---|---|---|
aria-label | string Human readable label for the dialog, in event the dialog title is not rendered | |
closeOnEscape | true | boolean Whether to close the dialog when the escape key is pressed |
closeOnInteractOutside | true | boolean Whether to close the dialog when the outside is clicked |
defaultOpen | false | boolean The initial open state of the dialog when rendered. Use when you don't need to control the open state of the dialog. |
finalFocusEl | () => MaybeElement Element to receive focus when the dialog is closed | |
id | string The unique identifier of the machine. | |
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. | |
immediate | boolean Whether to synchronize the present change immediately or defer it to the next frame | |
initialFocusEl | () => MaybeElement Element to receive focus when the dialog is opened | |
lazyMount | false | boolean Whether to enable lazy mounting |
modal | true | boolean Whether to prevent pointer interaction outside the element and hide all content below it |
onEscapeKeyDown | (event: KeyboardEvent) => void Function called when the escape key is pressed | |
onExitComplete | VoidFunction Function called when the animation ends in the closed state | |
onFocusOutside | (event: FocusOutsideEvent) => void Function called when the focus is moved outside the component | |
onInteractOutside | (event: InteractOutsideEvent) => void Function called when an interaction happens outside the component | |
onOpenChange | (details: OpenChangeDetails) => void Function to call when the dialog's open state changes | |
onPointerDownOutside | (event: PointerDownOutsideEvent) => void Function called when the pointer is pressed down outside the component | |
onRequestDismiss | (event: LayerDismissEvent) => void Function called when this layer is closed due to a parent layer being closed | |
open | boolean The controlled open state of the dialog | |
persistentElements | (() => Element | null)[] Returns the persistent elements that: - should not have pointer-events disabled - should not trigger the dismiss event | |
present | boolean Whether the node is present (controlled by the user) | |
preventScroll | true | boolean Whether to prevent scrolling behind the dialog when it's opened |
restoreFocus | boolean Whether to restore focus to the element that had focus before the dialog was opened | |
role | 'dialog' | 'dialog' | 'alertdialog' The dialog's role |
skipAnimationOnMount | false | boolean Whether to allow the initial presence animation. |
trapFocus | true | boolean Whether to trap focus inside the dialog when it's opened |
unmountOnExit | false | boolean Whether to unmount on exit. |
Backdrop
Prop | Default | Type |
---|---|---|
asChild | boolean Use the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
CSS Variable | Description |
---|---|
--layer-index | The index of the dismissable in the layer stack |
Data Attribute | Value |
---|---|
[data-scope] | dialog |
[data-part] | backdrop |
[data-state] | "open" | "closed" |
CloseTrigger
Prop | Default | Type |
---|---|---|
asChild | boolean Use the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
Content
Prop | Default | Type |
---|---|---|
asChild | boolean Use the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
CSS Variable | Description |
---|---|
--layer-index | The index of the dismissable in the layer stack |
--nested-layer-count | The number of nested dialogs |
Data Attribute | Value |
---|---|
[data-scope] | dialog |
[data-part] | content |
[data-state] | "open" | "closed" |
[data-nested] | dialog |
[data-has-nested] | dialog |
Description
Prop | Default | Type |
---|---|---|
asChild | boolean Use the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
Positioner
Prop | Default | Type |
---|---|---|
asChild | boolean Use the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
RootProvider
Prop | Default | Type |
---|---|---|
value | UseDialogReturn | |
asChild | boolean Use the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. | |
immediate | boolean Whether to synchronize the present change immediately or defer it to the next frame | |
lazyMount | false | boolean Whether to enable lazy mounting |
onExitComplete | VoidFunction Function called when the animation ends in the closed state | |
present | boolean Whether the node is present (controlled by the user) | |
skipAnimationOnMount | false | boolean Whether to allow the initial presence animation. |
unmountOnExit | false | boolean Whether to unmount on exit. |
Title
Prop | Default | Type |
---|---|---|
asChild | boolean Use the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
Trigger
Prop | Default | Type |
---|---|---|
asChild | boolean Use the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. |
Data Attribute | Value |
---|---|
[data-scope] | dialog |
[data-part] | trigger |
[data-state] | "open" | "closed" |
Accessibility
Complies with the Dialog WAI-ARIA design pattern.
Keyboard Support
Key | Description |
---|---|
Enter | When focus is on the trigger, opens the dialog. |
Tab | Moves focus to the next focusable element within the content. Focus is trapped within the dialog. |
Shift + Tab | Moves focus to the previous focusable element. Focus is trapped within the dialog. |
Esc | Closes the dialog and moves focus to trigger or the defined final focus element |