Dialog
A modal panel built on the native <dialog>, rendered as
<dialog is="root-dialog">.
Usage
<button is="root-button" type="button" data-variant="primary" data-color="default" command="show-modal" commandfor="dialog-usage">
Open dialog
</button>
<dialog is="root-dialog" id="dialog-usage" aria-labelledby="dialog-usage-title">
<header>
<h2 id="dialog-usage-title">Move to archive</h2>
<button is="root-button" type="button" data-variant="tertiary" data-color="neutral" square command="close" commandfor="dialog-usage" aria-label="Close">
<i is="root-icon" data-icon="x"></i>
</button>
<p>The project stays searchable and can be restored at any time.</p>
</header>
<section data-part="content">
<p>Archiving hides the project from the sidebar for everyone on the team.</p>
</section>
<footer>
<p>You can undo this from the archive.</p>
<div data-part="actions">
<button is="root-button" type="button" data-variant="secondary" data-color="default" command="close" commandfor="dialog-usage">Cancel</button>
<button is="root-button" type="button" data-variant="primary" data-color="default" command="close" commandfor="dialog-usage">Archive</button>
</div>
</footer>
</dialog>
Three bands, and they are the document's own elements: a <header>, the body marked
data-part="content"
- "everything between the two" is not a selector - and a
<footer>. Nothing is built by the component: a dialog is a container, and what goes in
it is the page's. The header's grid puts the close button beside the title and the description under both,
so the three are siblings in the markup and need no wrappers.
command
/ commandfor are the platform's invoker commands: the button opens and the buttons inside
close it, with no script of your own. Where they are not supported the dialog stays closed rather than
opening un-dismissable, so pair them with the native API - dialog.showModal() /
dialog.close() - if you have to support that.
Sizes
data-size
takes xs, small, medium and full; medium is the
default and needs no value written. Measured on the design: 400, 600 and 900px, and full is
1024x768 - the design's own full-screen modal, which stops growing rather than filling the viewport. Every
step is a maximum: below it the dialog shrinks with the screen, so a 900px dialog on a 600px one is a
600px dialog and never a horizontal scroll.
<button is="root-button" type="button" data-variant="secondary" data-color="default" command="show-modal" commandfor="dialog-xs">Open xs</button>
<button is="root-button" type="button" data-variant="secondary" data-color="default" command="show-modal" commandfor="dialog-small">Open small</button>
<button is="root-button" type="button" data-variant="secondary" data-color="default" command="show-modal" commandfor="dialog-medium">Open medium</button>
<button is="root-button" type="button" data-variant="secondary" data-color="default" command="show-modal" commandfor="dialog-full">Open full</button>
<dialog is="root-dialog" id="dialog-xs" data-size="xs" aria-labelledby="dialog-xs-title">
<header>
<h2 id="dialog-xs-title">Extra small</h2>
<button is="root-button" type="button" data-variant="tertiary" data-color="neutral" square command="close" commandfor="dialog-xs" aria-label="Close">
<i is="root-icon" data-icon="x"></i>
</button>
</header>
<section data-part="content"><p>400px, for a single question.</p></section>
</dialog>
<dialog is="root-dialog" id="dialog-small" data-size="small" aria-labelledby="dialog-small-title">
<header>
<h2 id="dialog-small-title">Small</h2>
<button is="root-button" type="button" data-variant="tertiary" data-color="neutral" square command="close" commandfor="dialog-small" aria-label="Close">
<i is="root-icon" data-icon="x"></i>
</button>
</header>
<section data-part="content"><p>600px, for a short form.</p></section>
</dialog>
<dialog is="root-dialog" id="dialog-medium" aria-labelledby="dialog-medium-title">
<header>
<h2 id="dialog-medium-title">Medium</h2>
<button is="root-button" type="button" data-variant="tertiary" data-color="neutral" square command="close" commandfor="dialog-medium" aria-label="Close">
<i is="root-icon" data-icon="x"></i>
</button>
</header>
<section data-part="content"><p>900px, the default, for content with a shape of its own.</p></section>
</dialog>
<dialog is="root-dialog" id="dialog-full" data-size="full" aria-labelledby="dialog-full-title">
<header>
<h2 id="dialog-full-title">Full screen</h2>
<button is="root-button" type="button" data-variant="tertiary" data-color="neutral" square command="close" commandfor="dialog-full" aria-label="Close">
<i is="root-icon" data-icon="x"></i>
</button>
</header>
<section data-part="content"><p>1024x768, for a task that takes over the page.</p></section>
<footer>
<div data-part="actions">
<button is="root-button" type="button" data-variant="primary" data-color="default" command="close" commandfor="dialog-full">Done</button>
</div>
</footer>
</dialog>
Bottom sheet
data-variant="sheet"
is the mobile declension the design draws: docked to the bottom edge, full width, rounded on the top
corners only. Three things change with it - a 1px Neutral 300 border instead of the elevation, because a
sheet touching the edge of the screen has nothing to cast a shadow onto; the two bands dropping their grey
so header, body and footer read as one surface; and the buttons stacking full width, which is what a thumb
can hit.
It is declared rather than inferred from the viewport. TODO: check with design - whether the switch should happen on its own below a breakpoint is still open.
<button is="root-button" type="button" data-variant="primary" data-color="default" command="show-modal" commandfor="dialog-sheet">
Open the sheet
</button>
<dialog is="root-dialog" id="dialog-sheet" data-variant="sheet" aria-labelledby="dialog-sheet-title">
<header>
<h2 id="dialog-sheet-title">Share this project</h2>
<button is="root-button" type="button" data-variant="secondary" data-color="default" square command="close" commandfor="dialog-sheet" aria-label="Close">
<i is="root-icon" data-icon="x"></i>
</button>
<p>Anyone with the link can read it.</p>
</header>
<section data-part="content">
<p>Sharing sends an email to everyone you pick.</p>
</section>
<footer>
<div data-part="actions">
<button is="root-button" type="button" data-variant="primary" data-color="default" command="close" commandfor="dialog-sheet">Copy link</button>
<button is="root-button" type="button" data-variant="secondary" data-color="default" command="close" commandfor="dialog-sheet">Invite people</button>
<button is="root-button" type="button" data-variant="tertiary" data-color="default" command="close" commandfor="dialog-sheet">Not now</button>
</div>
</footer>
</dialog>
Confirmation
role="alertdialog"
is for a choice that cannot be taken back. It keeps the backdrop click from closing it - dismissal has to
be a deliberate button - and it is the one case where the panel should not carry a close button either.
<button is="root-button" type="button" data-variant="primary" data-color="danger" command="show-modal" commandfor="dialog-delete">
Delete the project
</button>
<dialog is="root-dialog" id="dialog-delete" data-size="xs" role="alertdialog" aria-labelledby="dialog-delete-title">
<header>
<h2 id="dialog-delete-title">Delete this project?</h2>
<p>Everything in it goes with it, for everyone.</p>
</header>
<section data-part="content">
<p>This cannot be undone.</p>
</section>
<footer>
<div data-part="actions">
<button is="root-button" type="button" data-variant="secondary" data-color="default" command="close" commandfor="dialog-delete">Keep it</button>
<button is="root-button" type="button" data-variant="primary" data-color="danger" command="close" commandfor="dialog-delete">Delete</button>
</div>
</footer>
</dialog>
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
open |
boolean |
false |
Dialog visibility (native) |
data-size |
'xs' | 'small' | 'medium' | 'full' |
'medium' |
Maximum width, and a height for full |
data-variant |
'sheet' |
- | The bottom-sheet declension |
role |
'alertdialog' |
'dialog' |
A choice that cannot be taken back: no backdrop dismissal |
aria-labelledby |
string |
- | Points at the title; use aria-label when there is no visible one |
Parts
| Part | Element | Description |
|---|---|---|
header |
<header> |
Title, close button and description |
content |
<section> |
The body, and the only part that scrolls |
footer |
<footer> |
Description and [data-part="actions"] |
actions |
[data-part="actions"] |
The buttons, at the end of the line - stacked in a sheet |
Events
| Event | Detail | Description |
|---|---|---|
root-dialog.toggle |
{ state: 'open' | 'close' } |
Fired when the open attribute changes |
Accessibility
Native <dialog> already gives you initial focus movement, an
inert background,
Escape-to-close, and focus restoration on close - do not add a manual JS
focus trap on top; it is redundant and blocks the platform's own escape hatches. The one thing the
platform does not do is close on a backdrop click, so this component adds it - except for
role="alertdialog", as above. Page scroll is locked for as long as any modal
<dialog> is open (foundations/_reset.css). See
this writeup
for the reasoning.
Extends
HTMLDialogElement - use with <dialog is="root-dialog">.