Dialog
elements man ui/templates/dialogA modal window over the page that traps focus until it closes.
Exampleimport { Dialog, DialogFooter, DialogClose } from "@elements/ui/dialog"; import { Button } from "@elements/ui/button"; <Dialog title="Edit profile" description="Make changes, then save."> <Button slot="trigger">Edit profile</Button> <input value={form.name}> <DialogFooter> <DialogClose><Button>Cancel</Button></DialogClose> <Button variant="primary" onclick={() => save(form)}>Save</Button> </DialogFooter> </Dialog> <!-- controlled: open is two-way --> <Dialog open={state.editing} title="Edit profile">...</Dialog>
Parts
| Part | What it is |
|---|---|
Dialog |
The root: owns open, renders the trigger slot and, while open, the <dialog> overlay and panel. |
DialogTitle |
An <h2 class="dialog-title">; names the dialog (aria-labelledby). Use it instead of the title attribute for rich content. |
DialogDescription |
A <p class="dialog-description">; describes the dialog (aria-describedby). |
DialogHeader |
Stacks a title and description. |
DialogBody |
A column for the content between header and footer. |
DialogFooter |
Right-aligned actions; stacked full width on phones. closeButton appends an outline Close button. |
DialogClose |
Wraps any element; clicking it closes this dialog (not an outer one). |
Attributes
Dialog
| Attribute | Type | Default | Description |
|---|---|---|---|
open |
boolean (two-way) |
false |
Whether the dialog is open. Bind it (open={state.x}) to control it; the dialog writes false back when it closes itself. Omit it and the trigger slot opens it. |
title |
string |
"" |
Renders a DialogTitle in a header and names the dialog. |
description |
string |
"" |
Renders a DialogDescription under the title. |
label |
string |
"" |
aria-label for a dialog with no visible title. |
modal |
boolean | "trap-focus" |
true |
true: backdrop, inert page, focus trap, scroll lock. "trap-focus": focus is trapped but the page scrolls and takes clicks. false: the page stays usable and focus leaving closes it. |
dismissOnEscape |
boolean |
true |
Escape closes the dialog. |
dismissOnOutside |
boolean |
true |
A pointer press outside the panel closes the dialog. |
closeButton |
boolean |
true |
Shows the X button in the corner. |
closeLabel |
string |
"Close" |
The X button's accessible name. Set it in the page's language. |
initialFocus |
string | ((type: DialogInteraction) => HTMLElement | string | boolean | null | void) |
"" |
CSS selector, inside the dialog, of the element to focus on open. Default: [autofocus], else the first focusable element that is not a link, else the panel. A function gets how the dialog opened (mouse, touch, pen, keyboard, or "" when the caller opened it) and returns an element or selector, true or null for the default, or false (or nothing) to leave focus alone. |
returnFocus |
boolean |
true |
Return focus to the trigger (or whatever had focus) when the dialog closes. Opened from a menu item, focus returns to the menu's button. |
finalFocus |
string | ((type: DialogInteraction) => HTMLElement | string | boolean | null | void) |
"" |
CSS selector, anywhere on the page, of the element to focus on close instead of the trigger. A function gets how the dialog closed and returns an element or selector, true or null for the default, or false (or nothing) to leave focus alone. |
triggers |
string |
"" |
CSS selector, anywhere on the page, of buttons outside the dialog that open it through a controlled open. Each says it opens a dialog, and only the one that opened it is marked open (aria-expanded, data-popup-open); focus returns to it. |
size |
"sm" | "md" | "lg" | "xl" | "full" |
"md" |
Panel max width. md is the theme's --dialog-width. |
scroll |
"outside" | "inside" |
"outside" |
outside: a tall dialog scrolls with the overlay. inside: the panel fits the viewport, the header and footer stay, and DialogBody scrolls. |
class |
string |
"" |
Extra classes on the panel. |
onOpenChange |
(open: boolean, reason?: DialogChangeReason) => void |
() => {} |
Called when the dialog opens or closes itself, with why: trigger-press, escape-key, outside-press, close-press, focus-out, close-watcher (a platform close request, such as Android's back gesture) or none. Not called for changes the caller makes. |
onOpenChangeComplete |
(open: boolean) => void |
() => {} |
Called after the enter animation (true) and after the exit, once closed (false). |
onEscapeKeyDown |
(e: KeyboardEvent) => void |
() => {} |
Called on Escape before closing; e.preventDefault() keeps it open, and e.allowPropagation() lets the Escape reach the page's own key handlers after it closes the dialog. |
onPointerDownOutside |
(e: PointerEvent) => void |
() => {} |
Called on a press outside before closing; e.preventDefault() keeps it open. |
onInteractOutside |
(e: Event) => void |
() => {} |
Called on a press outside, or focus moving outside a non-modal dialog, before closing; e.preventDefault() keeps it open. |
onOpenAutoFocus |
(e: Event) => void |
() => {} |
Called before focus moves in on open; e.preventDefault() leaves focus to the handler. |
onCloseAutoFocus |
(e: Event) => void |
() => {} |
Called before focus returns on close; e.preventDefault() leaves focus to the handler. |
DialogFooter
| Attribute | Type | Default | Description |
|---|---|---|---|
closeButton |
boolean |
false |
Appends an outline Close button that closes the dialog. |
closeLabel |
string |
"Close" |
The footer Close button's text. Set it in the page's language. |
DialogTitle, DialogDescription, DialogHeader, DialogBody, DialogFooter
| Attribute | Type | Default | Description |
|---|---|---|---|
class |
string |
"" |
Extra classes. |
Slots
| Slot | Content |
|---|---|
trigger |
The element that opens the dialog, usually a <Button>. It gets aria-haspopup, aria-expanded, aria-controls and data-state. |
default |
The dialog's content, below the header. |
Keyboard
| Keys | Action |
|---|---|
Space / Enter |
On the trigger: opens the dialog, or closes it when open (a non-modal dialog leaves its trigger reachable). On the close button or a DialogClose button: closes it. |
Tab |
Moves to the next focusable element inside; wraps from the last to the first. |
Shift + Tab |
Moves to the previous focusable element; wraps from the first to the last. |
Escape |
Closes the dialog (unless dismissOnEscape is false or onEscapeKeyDown prevents it). In nested dialogs, only the top one. |
Data attributes
| Attribute | Values | On | Description |
|---|---|---|---|
data-state |
"open" | "closed" |
trigger, .dialog-overlay, .dialog | closed during the exit animation. |
data-modal |
"true" | "trap-focus" | "false" |
.dialog-overlay | |
data-nested |
present | .dialog | The dialog is inside another dialog. |
data-nested-dialog-open |
present | .dialog | A dialog inside this one is open. |
--nested-dialogs |
number | .dialog | How many dialogs are open inside this one. |
data-layer |
stack depth | .dialog | Set while the dialog is a dismiss layer. |
data-scroll-locked |
present | <body> |
While any modal dialog is open. |
Accessibility
- The overlay is a native
<dialog>opened with showModal(): it has role dialog, sits in the top layer, and the rest of the page is inert to pointer, focus and assistive technology. - aria-modal="true" (modal only), aria-labelledby (the title) or aria-label (label), and aria-describedby (the description, omitted without one) are set on the
<dialog>. - Focus moves into the dialog on open, is trapped while modal, and returns to the trigger on close; when the dialog was opened from elsewhere it returns to the element that had focus.
- The trigger gets aria-haspopup="dialog", aria-expanded and aria-controls.
- The close button is an icon button with aria-label="Close".
See also
- Button (opens in a new tab): A button or a link that looks like one, in six variants and four sizes.