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 }.

PropTypeDefaultDescription
defaultOpenbooleanfalseUncontrolled initial open. Use open / v-model:open for a controlled dialog.
openboolean—Controlled open state (v-model:open).
handleBaseDialogHandle—Links detached triggers. Create with createDialogHandle().
triggerIdstring | null—Active trigger id in controlled mode.
disablePointerDismissalbooleanfalseDo not close on backdrop (outside) press. Escape and Close still work.
EventTypeDescription
@update:open(open: boolean) => voidEmitted when the dialog opens or closes.

Trigger

Button that opens the dialog. Renders a <button>.

PropTypeDefaultDescription
disabledboolean—Ignore presses. Sets data-disabled.
handleBaseDialogHandle—Detached handle. Required when the trigger is outside Root.
payloadunknown—Value exposed on Root’s slot when this trigger opens the dialog.
idstringgeneratedTrigger id. Used with Root triggerId.
AttributeDescription
data-popup-openPresent when this trigger’s dialog is open.
data-disabledPresent when the trigger is disabled.

Portal

Teleports to body while the overlay is present (useCssPresence).

AttributeDescription
data-openPresent when the dialog is open.
data-closedPresent while present but not open (exit transition).
data-nestedPresent when nested in another overlay.
data-starting-stylePresent while animating in.
data-ending-stylePresent 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.

PropTypeDefaultDescription
forceRenderboolean—Render even when nested.
AttributeDescription
data-openPresent when the dialog is open.
data-closedPresent when the dialog is closed.
data-starting-stylePresent while animating in.
data-ending-stylePresent while animating out.

Viewport

A positioning container for the popup. Can be made scrollable (see Outside scroll). Centers the popup by default.

AttributeDescription
data-openPresent when the dialog is open.
data-closedPresent when the dialog is closed.
data-nestedPresent when nested in another overlay.
data-nested-dialog-openPresent when a nested dialog is open.
data-starting-stylePresent while animating in.
data-ending-stylePresent while animating out.

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.

PropTypeDefaultDescription
initialFocusHTMLElement | false | nullfirst tabbableElement to focus on open. false skips moving focus.
finalFocusHTMLElement | false | nullprevious elementElement to focus on close. false skips restoring focus.
AttributeDescription
data-openPresent when the dialog is open.
data-closedPresent when the dialog is closed.
data-nestedPresent when nested in another overlay.
data-nested-dialog-openPresent when a nested dialog is open.
data-starting-stylePresent while animating in.
data-ending-stylePresent while animating out.
VariableDescription
--nested-dialogsHow many dialogs are nested inside this one.

Title

Heading that labels the dialog. Renders an <h2>.

PropTypeDefaultDescription
idstringgeneratedOverride 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.

PropTypeDefaultDescription
disabledboolean—Ignore presses. Sets data-disabled.
AttributeDescription
data-disabledPresent 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()
FieldDescription
openRef of whether the dialog is open.
payloadRef of the active trigger payload.
triggerIdRef of the active trigger id.
show(payload?, triggerId?)Open the dialog.
hide()Close the dialog.