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.

Previewindex.html
Edit profile Edit profile Make changes to your profile here. Click save when you are done. Name Cancel Save changes
<!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>.

Previewindex.html
Share Share link Anyone with this link can view the document. Done
<!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.

Previewindex.htmldialog-script-demo.html
Change plan Choose a plan You can switch plans at any time. Free Pro Current plan:
<!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.

Previewindex.htmldialog-side-demo.html
Top Right Bottom Left Notifications Choose what you want to be notified about. Close
<!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.

Previewindex.html
Accept terms Terms of service Please accept the updated terms to continue. Decline Accept
<!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.

Previewindex.htmldialog-cancel-demo.html
Write a note New note Type something, then try Escape or the close button. Save
<!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

Edit this page on GitHub · View as Markdown