Dialog
Dialog — a modal, built on <dialog> and opened with an invoker command.
Examples
<button class="btn btn-outline" type="button"
command="show-modal" commandfor="demo-dialog-closedby">What's new</button>
<dialog class="dialog" id="demo-dialog-closedby" closedby="any"
aria-labelledby="demo-dialog-closedby-title">
<header class="dialog-header">
<h2 class="dialog-title" id="demo-dialog-closedby-title">What's new</h2>
<button class="btn btn-quiet btn-icon dialog-close" type="button"
command="close" commandfor="demo-dialog-closedby" aria-label="Close">
<svg class="icon" aria-hidden="true"><use href="#icon-x"></use></svg>
</button>
</header>
<div class="dialog-body">
<p>Click anywhere outside this panel and it closes. No script, no handler.</p>
</div>
</dialog>closedby="any" is the platform's own light dismiss, and it is why this component ships no module for it.
Three values: "any" closes on Escape and on a click outside, "closerequest" is the default for a modal and closes on Escape only, "none" refuses both and leaves the close button as the only way out. It is opt-in per dialog, and that is the point — a modal holding a half-filled form must not vanish because someone clicked past its edge, and neither must an alert dialog asking you to confirm something destructive. Where the attribute is not supported the dialog simply keeps the default behaviour: Escape works, clicking outside does nothing, and nothing is broken. That is the whole degradation, which is why there is no polyfill here — a script that reproduced it would have to run on every dialog on the page to catch the one that wanted it. One thing to know before setting it: the attribute closes on the pointerup, and the compatibility click a touchscreen sends afterwards is hit-tested once the dialog has left the top layer, so under a finger the tap that dismissed the dialog also lands on the page behind — on this very page it follows the link it was over. A mouse never does this. Where the dialog can open over something clickable, close it from a click handler on the backdrop instead; command-palette is the worked example.
<button class="btn btn-primary" type="button"
command="show-modal" commandfor="demo-dialog">Invite people</button>
<dialog class="dialog dialog-divided" id="demo-dialog" aria-labelledby="demo-dialog-title">
<header class="dialog-header">
<h2 class="dialog-title" id="demo-dialog-title">Invite people</h2>
<button class="btn btn-quiet btn-icon dialog-close" type="button"
command="close" commandfor="demo-dialog" aria-label="Close">
<svg class="icon" aria-hidden="true"><use href="#icon-x"></use></svg>
</button>
</header>
<div class="dialog-body">
<div class="field">
<label class="field-label" for="demo-dialog-email">Email addresses</label>
<input class="input" id="demo-dialog-email" name="emails" type="text"
placeholder="ada@example.com, grace@example.com" />
<p class="field-help" id="demo-dialog-help">Separate several with commas.</p>
</div>
</div>
<footer class="dialog-footer">
<button class="btn btn-outline" type="button"
command="close" commandfor="demo-dialog">Cancel</button>
<button class="btn btn-primary" type="button"
command="close" commandfor="demo-dialog">Send invitations</button>
</footer>
</dialog>No script runs on this page. command="show-modal" calls showModal(), which brings the top layer, the backdrop, inert on the rest of the page, a focus trap, Escape, and focus returned to the button — all from the browser.
<button class="btn btn-danger" type="button"
command="show-modal" commandfor="demo-alert">Delete project</button>
<dialog class="dialog dialog-alert" id="demo-alert" role="alertdialog"
aria-labelledby="demo-alert-title" aria-describedby="demo-alert-text">
<div class="dialog-body">
<h2 class="dialog-title" id="demo-alert-title">Delete this project?</h2>
<p id="demo-alert-text">Everything in it goes with it. This cannot be undone.</p>
</div>
<footer class="dialog-footer">
<button class="btn btn-outline" type="button"
command="close" commandfor="demo-alert">Keep it</button>
<button class="btn btn-danger" type="button"
command="close" commandfor="demo-alert">Delete</button>
</footer>
</dialog>role="alertdialog" and aria-describedby are the consumer's job — CSS cannot add a role, and an alert that announces itself as a plain dialog is worse than one that merely looks wrong.
Tokens 17
Level 2, declared on .dialog itself. Set any of them on that selector to restyle this component without touching the skin.
| Token | Default |
|---|---|
| --dialog-backdrop | var(--color-overlay) |
| --dialog-backdrop-filter | none |
| --dialog-divider-color | var(--color-border-subtle) |
| --dialog-duration | var(--duration-normal) |
| --dialog-footer-gap | var(--space-2) |
| --dialog-title-font-family | var(--font-primary) |
| --dialog-title-font-size | var(--font-size-lg) |
| --dialog-title-font-weight | var(--font-weight-semibold) |
| --dialog-bg | var(--color-surface-raised) |
| --dialog-border-color | var(--color-border-subtle) |
| --dialog-border-width | var(--border-width) |
| --dialog-color | var(--color-on-surface) |
| --dialog-gap | var(--space-4) |
| --dialog-inline-size | 32rem |
| --dialog-padding | var(--space-6) |
| --dialog-radius | var(--radius-box) |
| --dialog-shadow | var(--elevation-overlay) |
Variants and states
Variants
.dialog-lg.dialog-sm
Inside it
.dialog-body.dialog-footer.dialog-alert.dialog-divided.dialog-header.dialog-close.dialog-full.dialog-title
State it reads
Read from the platform, never mirrored into a class that could disagree with it.
[open]data-legacy
Before you ship it
What you have to do 1 requirements
- Name the dialog:
aria-labelledbypointing at.dialog-title, or anaria-label. A modal with no accessible name is announced as "dialog" and nothing else. Give the close button a name too — it is icon-only.
src/css/components/dialog.css · npx mostlycss add dialog