Dialog
A popup that opens on top of the entire page. Unlike Alert Dialog, it closes on backdrop click. The trigger and close match Button. The popup has no stroke. The title is Light and the description is Regular.
Usage guidelines
- Dialog doesn’t support gestures. Use Drawer when you need gesture support or snap points. A panel that slides in from the edge of the screen and doesn’t need gesture support is a positioned Dialog.
Anatomy
<UiDialogRoot>
<UiDialogTrigger />
<UiDialogPortal>
<UiDialogBackdrop />
<UiDialogViewport>
<UiDialogPopup>
<UiDialogTitle />
<UiDialogDescription />
<UiDialogClose />
</UiDialogPopup>
</UiDialogViewport>
</UiDialogPortal>
</UiDialogRoot>
Examples
State
By default, Dialog is an uncontrolled component that manages its own state.
<UiDialogRoot>
<UiDialogTrigger>Open</UiDialogTrigger>
<UiDialogPortal>
<UiDialogPopup>
<UiDialogTitle>Example dialog</UiDialogTitle>
<UiDialogClose>Close</UiDialogClose>
</UiDialogPopup>
</UiDialogPortal>
</UiDialogRoot>
Use open / v-model:open if you need to access or control the state. For example, close it after a form submits, or open it from another place in the app.
<script setup>
const open = ref(false)
</script>
<template>
<UiDialogRoot v-model:open="open">
<UiDialogTrigger>Open</UiDialogTrigger>
<UiDialogPortal>
<UiDialogPopup>
<form @submit.prevent="open = false">
…
</form>
</UiDialogPopup>
</UiDialogPortal>
</UiDialogRoot>
</template>
It’s also common to handle @update:open when the app needs to do something as the dialog opens or closes. Prefer that over watching open in an effect.
Open from a menu
Control the dialog and open it from a menu item click. The menu closes first; the dialog stays until Close, Escape, or a backdrop click.
Nested dialogs
You can nest dialogs within one another. Use [data-nested-dialog-open] and var(--nested-dialogs) to style the parent. Child backdrops are not rendered, so the parent stays visible behind the one on top. The parent popup scales with calc(1 - 0.1 * var(--nested-dialogs)).
Close confirmation
Nest an Alert Dialog inside a controlled Dialog. If the parent would close while the textarea has text, keep the dialog open and show the alert instead. Shown on backdrop click, Escape, or Close.
Custom focus management
Control where focus goes when the dialog opens and closes with initialFocus and finalFocus on UiDialogPopup. Set either to false to skip moving focus. Here the Feedback field is focused on open, and Final focus is focused on close.
Outside scroll dialog
Make the dialog scrollable by using UiDialogViewport as an outer scroll container. The popup can extend past the bottom edge. The scrollable area uses Scroll Area for a custom scrollbar.
Inside scroll dialog
Keep the popup fully on screen and scroll an inner container. Viewport positions the popup; Scroll Area scrolls the body.
Placing elements outside the popup
Keep outside chrome (the close control) inside UiDialogPopup so it stays in the tab order and is announced correctly. Style an inner child as the colored surface. Set the popup to pointer-events: none and the inner surface / close button to pointer-events: auto so backdrop clicks still register.
Detached triggers
For a one-off, put UiDialogTrigger inside Root. When that is impractical, create a handle with createDialogHandle() and pass it to both the trigger and the root.
<script setup>
import { createDialogHandle } from '~/lib/ui/dialog'
const demoDialog = createDialogHandle()
</script>
<template>
<UiDialogTrigger :handle="demoDialog">Open</UiDialogTrigger>
<UiDialogRoot :handle="demoDialog">
…
</UiDialogRoot>
</template>
Multiple triggers
One dialog can be opened by several triggers. Place multiple triggers inside Root, or give detached triggers the same handle. Pass payload on the trigger and read it from Root’s default slot.
<UiDialogRoot v-slot="{ payload }">
<UiDialogTrigger :payload="{ text: 'Trigger 1' }">Trigger 1</UiDialogTrigger>
<UiDialogTrigger :payload="{ text: 'Trigger 2' }">Trigger 2</UiDialogTrigger>
…
</UiDialogRoot>
Controlled mode with multiple triggers
Use v-model:open (or open / @update:open) with a handle. Set id on each trigger and triggerId on Root so the matching trigger receives data-popup-open. Opening from a plain button calls handle.show(payload, id).
API reference
Root
Groups all parts of the dialog. Renders nothing of its own. Default slot receives { payload, triggerId }.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen | boolean | false | Uncontrolled initial open. Use open / v-model:open for a controlled dialog. |
open | boolean | — | Controlled open state (v-model:open). |
handle | BaseDialogHandle | — | Links detached triggers. Create with createDialogHandle(). |
triggerId | string | null | — | Active trigger id in controlled mode. |
disablePointerDismissal | boolean | false | Do not close on backdrop (outside) press. Escape and Close still work. |
| Event | Type | Description |
|---|---|---|
@update:open | (open: boolean) => void | Emitted when the dialog opens or closes. |
Trigger
Button that opens the dialog. Renders a <button>.
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | — | Ignore presses. Sets data-disabled. |
handle | BaseDialogHandle | — | Detached handle. Required when the trigger is outside Root. |
payload | unknown | — | Value exposed on Root’s slot when this trigger opens the dialog. |
id | string | generated | Trigger id. Used with Root triggerId. |
| Attribute | Description |
|---|---|
data-popup-open | Present when this trigger’s dialog is open. |
data-disabled | Present when the trigger is disabled. |
Portal
Teleports to body while the overlay is present (useCssPresence).
| Attribute | Description |
|---|---|
data-open | Present when the dialog is open. |
data-closed | Present while present but not open (exit transition). |
data-nested | Present when nested in another overlay. |
data-starting-style | Present while animating in. |
data-ending-style | Present while animating out. |
Backdrop
Covers the viewport. Click dismisses unless Root disablePointerDismissal is set. Nested dialogs skip rendering the child backdrop unless forceRender is set. Uses position: absolute inside the fixed portal.
| Prop | Type | Default | Description |
|---|---|---|---|
forceRender | boolean | — | Render even when nested. |
| Attribute | Description |
|---|---|
data-open | Present when the dialog is open. |
data-closed | Present when the dialog is closed. |
data-starting-style | Present while animating in. |
data-ending-style | Present while animating out. |
Viewport
A positioning container for the popup. Can be made scrollable (see Outside scroll). Centers the popup by default.
| Attribute | Description |
|---|---|
data-open | Present when the dialog is open. |
data-closed | Present when the dialog is closed. |
data-nested | Present when nested in another overlay. |
data-nested-dialog-open | Present when a nested dialog is open. |
data-starting-style | Present while animating in. |
data-ending-style | Present while animating out. |
Popup
role="dialog". Escape closes. Focus stays inside until close. Enter and leave use data-starting-style / data-ending-style (scale 0.98, 100ms). No animation library. No drop-shadow.
| Prop | Type | Default | Description |
|---|---|---|---|
initialFocus | HTMLElement | false | null | first tabbable | Element to focus on open. false skips moving focus. |
finalFocus | HTMLElement | false | null | previous element | Element to focus on close. false skips restoring focus. |
| Attribute | Description |
|---|---|
data-open | Present when the dialog is open. |
data-closed | Present when the dialog is closed. |
data-nested | Present when nested in another overlay. |
data-nested-dialog-open | Present when a nested dialog is open. |
data-starting-style | Present while animating in. |
data-ending-style | Present while animating out. |
| Variable | Description |
|---|---|
--nested-dialogs | How many dialogs are nested inside this one. |
Title
Heading that labels the dialog. Renders an <h2>.
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | generated | Override the label id used by the popup. |
Description
Supporting text. Renders a <p>.
Close
Button that sets open to false. Add class="cta" for the stronger wash.
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | — | Ignore presses. Sets data-disabled. |
| Attribute | Description |
|---|---|
data-disabled | Present when the button is disabled. |
createDialogHandle
Creates a handle that connects UiDialogRoot with detached UiDialogTrigger components.
const handle = createDialogHandle<{ text: string }>()
handle.show({ text: 'Trigger 2' }, 'dialog-trigger-2')
handle.hide()
| Field | Description |
|---|---|
open | Ref of whether the dialog is open. |
payload | Ref of the active trigger payload. |
triggerId | Ref of the active trigger id. |
show(payload?, triggerId?) | Open the dialog. |
hide() | Close the dialog. |