Dialog
ui-dialog renders a native <dialog> in its shadow root and opens it with showModal(). The browser puts it in the
top layer, makes the rest of the page inert, moves focus into the dialog and returns focus to the trigger when it
closes. Escape, a click on the backdrop and the built-in close button all ask the dialog to close. A cancelable
cancel event lets you keep it open.
Build it from parts: ui-dialog-trigger opens it, ui-dialog-header holds the ui-dialog-title and
ui-dialog-description, the content goes in between, and ui-dialog-footer holds the actions. ui-dialog-close
closes the dialog when the element inside it is clicked.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dialog demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/dialog.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/field.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/input.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/label.html"></import-element>
<ui-dialog>
<ui-dialog-trigger><ui-button variant="outline">Edit profile</ui-button></ui-dialog-trigger>
<ui-dialog-header>
<ui-dialog-title>Edit profile</ui-dialog-title>
<ui-dialog-description>Make changes to your profile here. Click save when you are done.</ui-dialog-description>
</ui-dialog-header>
<ui-field>
<ui-label for="dialog-profile-name">Name</ui-label>
<ui-input id="dialog-profile-name" value="Pedro Duarte"></ui-input>
</ui-field>
<ui-dialog-footer>
<ui-dialog-close><ui-button variant="outline">Cancel</ui-button></ui-dialog-close>
<ui-dialog-close value="saved"><ui-button>Save changes</ui-button></ui-dialog-close>
</ui-dialog-footer>
</ui-dialog>
</body>
</html>Examples
Open and close with commands
The trigger part is the usual way to open the dialog. A button elsewhere on the page can open it too, because ui-dialog
listens for the command event that commandfor buttons send. It understands --show-modal, --close and
--request-close, with or without the -- prefix. --close closes straight away. --request-close fires cancel
first, like Escape does. ui-button accepts either form. On a plain <button>, use the -- form, because browsers with native
invoker commands only forward custom (--) commands to elements that are not a native <dialog>.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dialog demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/dialog.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/input.html"></import-element>
<ui-button commandfor="dialog-commands" command="--show-modal">Share</ui-button>
<ui-dialog id="dialog-commands">
<ui-dialog-header>
<ui-dialog-title>Share link</ui-dialog-title>
<ui-dialog-description>Anyone with this link can view the document.</ui-dialog-description>
</ui-dialog-header>
<ui-input value="https://example.com/docs/a1b2c3" aria-label="Link" readonly></ui-input>
<ui-dialog-footer>
<ui-button variant="secondary" commandfor="dialog-commands" command="--request-close">Done</ui-button>
</ui-dialog-footer>
</ui-dialog>
</body>
</html>Read the result
ui-dialog-close value="…" closes the dialog and stores the value in returnValue. From script, close(value) does
the same. The close event fires after the dialog has closed. Escape, the backdrop and the close button close it
without changing returnValue.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dialog demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/dialog.html"></import-element>
<import-element from="./dialog-script-demo.html"></import-element>
<dialog-script-demo></dialog-script-demo>
</body>
</html><custom-element name="dialog-script-demo">
<script>
const plan = useState({ current: "Free" });
function closed() {
plan.current = refs.dialog.returnValue || plan.current;
}
</script>
<style>
:host {
display: flex;
align-items: center;
gap: calc(var(--spacing) * 4);
font-size: var(--text-sm);
}
</style>
<ui-dialog ref="dialog" onclose="closed()">
<ui-dialog-trigger><ui-button variant="outline">Change plan</ui-button></ui-dialog-trigger>
<ui-dialog-header>
<ui-dialog-title>Choose a plan</ui-dialog-title>
<ui-dialog-description>You can switch plans at any time.</ui-dialog-description>
</ui-dialog-header>
<ui-dialog-footer>
<ui-dialog-close value="Free"><ui-button variant="outline">Free</ui-button></ui-dialog-close>
<ui-dialog-close value="Pro"><ui-button>Pro</ui-button></ui-dialog-close>
</ui-dialog-footer>
</ui-dialog>
<span>Current plan: <strong><slot name="#plan.current"></slot></strong></span>
</custom-element>Side placement
side turns the dialog into a panel attached to an edge of the viewport: top, right, bottom or left. The
default is center. Sheet is a shorthand for this.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dialog demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/dialog.html"></import-element>
<import-element from="./dialog-side-demo.html"></import-element>
<dialog-side-demo></dialog-side-demo>
</body>
</html><custom-element name="dialog-side-demo">
<script>
const panel = useState({ side: "right" });
function open(side) {
panel.side = side;
refs.dialog.showModal();
}
</script>
<style>
:host {
display: flex;
flex-wrap: wrap;
gap: calc(var(--spacing) * 2);
}
</style>
<ui-button variant="outline" onclick="open('top')">Top</ui-button>
<ui-button variant="outline" onclick="open('right')">Right</ui-button>
<ui-button variant="outline" onclick="open('bottom')">Bottom</ui-button>
<ui-button variant="outline" onclick="open('left')">Left</ui-button>
<ui-dialog ref="dialog" _side="#panel.side">
<ui-dialog-header>
<ui-dialog-title>Notifications</ui-dialog-title>
<ui-dialog-description>Choose what you want to be notified about.</ui-dialog-description>
</ui-dialog-header>
<ui-dialog-footer>
<ui-dialog-close><ui-button>Close</ui-button></ui-dialog-close>
</ui-dialog-footer>
</ui-dialog>
</custom-element>Persistent
persistent ignores Escape and backdrop clicks, so the dialog only closes through its own buttons. Combine it with
hide-close to remove the close button in the corner.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dialog demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/dialog.html"></import-element>
<ui-dialog persistent hide-close>
<ui-dialog-trigger><ui-button variant="outline">Accept terms</ui-button></ui-dialog-trigger>
<ui-dialog-header>
<ui-dialog-title>Terms of service</ui-dialog-title>
<ui-dialog-description>Please accept the updated terms to continue.</ui-dialog-description>
</ui-dialog-header>
<ui-dialog-footer>
<ui-dialog-close value="declined"><ui-button variant="outline">Decline</ui-button></ui-dialog-close>
<ui-dialog-close value="accepted"><ui-button>Accept</ui-button></ui-dialog-close>
</ui-dialog-footer>
</ui-dialog>
</body>
</html>Keep it open on cancel
Escape, a backdrop click, the close button, requestClose() and the --request-close command fire a cancelable
cancel event before closing. Call preventDefault() to keep the dialog open, for example while a form has unsaved
changes. close(), --close and ui-dialog-close skip this step.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dialog demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/dialog.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/textarea.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/toaster.html"></import-element>
<ui-toaster></ui-toaster>
<import-element from="./dialog-cancel-demo.html"></import-element>
<dialog-cancel-demo></dialog-cancel-demo>
</body>
</html><custom-element name="dialog-cancel-demo">
<script>
function cancelled(event) {
if (!refs.note.value) return;
event.preventDefault();
document.dispatchEvent(new CustomEvent("toast", { detail: { title: "Save or clear your note first" } }));
}
function save() {
refs.note.value = "";
}
</script>
<ui-dialog oncancel="cancelled(event)">
<ui-dialog-trigger><ui-button variant="outline">Write a note</ui-button></ui-dialog-trigger>
<ui-dialog-header>
<ui-dialog-title>New note</ui-dialog-title>
<ui-dialog-description>Type something, then try Escape or the close button.</ui-dialog-description>
</ui-dialog-header>
<ui-textarea ref="note" aria-label="Note" placeholder="Remember to..."></ui-textarea>
<ui-dialog-footer>
<ui-dialog-close value="saved"><ui-button onclick="save()">Save</ui-button></ui-dialog-close>
</ui-dialog-footer>
</ui-dialog>
</custom-element>API
Parts
| Part | Slot | Renders |
|---|---|---|
ui-dialog |
The modal <dialog>, a grid with a gap between the header, content and footer, plus the close button. |
|
ui-dialog-trigger |
trigger |
Its content, outside the dialog. A click on it focuses the element inside and opens the dialog. |
ui-dialog-header |
header |
A column for the title and description at the top of the dialog. |
ui-dialog-title |
(default) | An <h2> in var(--text-lg) semibold. Its text becomes the dialog's accessible name. |
ui-dialog-description |
(default) | A <p> in muted var(--text-sm). Its text becomes the dialog's accessible description. |
ui-dialog-footer |
footer |
A row of actions at the bottom, aligned to the end and wrapped on narrow screens. |
ui-dialog-close |
(default) | Its content. A click on it closes the dialog without firing cancel. |
Parts with a slot assign themselves to it, so you do not write slot. Everything else goes in the default slot, the
dialog body, in source order.
ui-dialog attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
open |
boolean | false |
Whether the dialog is open. Setting it opens the dialog modally. Reflects. |
side |
"center" | "top" | "right" | "bottom" | "left" |
"center" |
Where the dialog sits. Any value other than center attaches it to that edge as a full-height or full-width panel. |
hide-close |
boolean | false |
Hides the close button in the top corner. Property name: hideClose. |
persistent |
boolean | false |
Ignores Escape and backdrop clicks. The close button and requestClose() still work. |
label |
string | Accessible name when there is no ui-dialog-title. |
|
description |
string | Accessible description when there is no ui-dialog-description. |
ui-dialog-close attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
value |
string | Stored in the dialog's returnValue when it closes. Leave it out to keep the previous value. |
Properties and methods
| Name | Description |
|---|---|
showModal() |
Opens the dialog modally. |
show() |
Same as showModal(). The dialog is always modal. |
close(value?) |
Closes the dialog. When value is given it becomes returnValue. Does not fire cancel. |
requestClose(value?) |
Fires cancel, then closes with value unless the event was cancelled. |
returnValue |
Read-only. The value passed to the last close(value) or ui-dialog-close, or "". |
open |
Boolean property mirroring the attribute. |
Commands
| Command | Action |
|---|---|
--show-modal |
Opens the dialog. |
--close |
Closes the dialog. |
--request-close |
Same as requestClose(). |
ui-button also accepts show-modal, close and request-close without the prefix.
Events
| Event | Detail | Description |
|---|---|---|
cancel |
none | Fired before the dialog closes from Escape, a backdrop click, the close button, requestClose() or --request-close. Cancelable. Does not bubble. |
close |
none | Fired after the dialog has closed, however it was closed. Does not bubble. |
CSS parts
| Element | Part | Description |
|---|---|---|
ui-dialog |
dialog |
The native <dialog> element. Style the backdrop with ui-dialog::part(dialog)::backdrop. |
ui-dialog |
body |
Wrapper around the default slot. In a centred dialog it adds no box of its own (display: contents), so empty content leaves no gap. In a left or right sheet it fills the remaining height. |
ui-dialog |
close |
The close button in the top corner. |
ui-dialog-title |
title |
The <h2>. |
ui-dialog-description |
description |
The <p>. |
ui-dialog-trigger |
trigger |
The wrapper that listens for clicks. |
ui-dialog-close |
close |
The wrapper that listens for clicks. |
Accessibility
- The inner
<dialog>is opened withshowModal(), so the page behind it is inert and focus is trapped inside. - On open, focus moves to the first focusable element, or to an element with
autofocus. On close it returns to the trigger. - The text of
ui-dialog-titleandui-dialog-descriptionbecomes the dialog'saria-labelandaria-description, and updates when it changes. The title is also an<h2>heading. - Escape closes the dialog unless
persistentis set or thecancelevent is cancelled. - The close button has
aria-label="Close".