Components
Dialog
A minimally styled window overlaid on the main content. By design the Dialog is minimal with zero content to allow for both modal and non-modal use.
Full support Supported since v135. Partial support
Missing:
container-style-queries, overlay.
Partial support
Missing: dialog-closedby, overlay.
Modal vs Dialog
The term "modal" and "dialog" are often used interchangeably, but there's an important difference. A modal window describes parts of a UI that blocks user interaction. A dialog doesn't have to be blocking.
Usage
Non-modal
- Toast: informative but non-interruptive
Modal
No JavaScript required
In browsers that support Invoker Commands you can toggle a <dialog> without JavaScript by using
the commandfor and command attributes.
<button commandfor="example-dialog-html" command="show-modal" class="button outlined"> Open dialog</button>
<dialog id="example-dialog-html" class="dialog card elevated" role="alertdialog" aria-labelledby="dialog-heading" aria-modal="true"> <hgroup> <h2 id="dialog-heading" class="h4">Are you sure?</h2> </hgroup> <div class="content"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sodales, nulla sit amet porttitor rhoncus. </div> <div class="actions"> <button commandfor="example-dialog-html" command="close" class="button" type="button"> Cancel </button> <button commandfor="example-dialog-html" command="close" class="button filled" type="button"> Save </button> </div></dialog>How to close a dialog
You can use it like this: <dialog closedby=""> and give
it the following values:
| Attr value | Description |
|---|---|
closedby="any" | Click anywhere outside of the dialog to close it. |
closedby="closerequest" | Device-specific way to close, ex: Esc on desktop, back button on mobile, and whatever dismiss action assistive tools use. |
closedby="none" | You have to handroll a closing solution yourself. |
<button commandfor="closing-behaviors-dialog-html" command="show-modal" class="button outlined"> Open dialog</button>
<dialog id="closing-behaviors-dialog-html" class="dialog card elevated" closedby="any"> <hgroup> <h2 class="h4">How to close</h2> </hgroup> <div class="content"> <div class="fieldset"> <p class="legend">Choose a closing behavior:</p> <div class="field-group" role="group"> <label class="radio"> <input type="radio" name="closedby-demo-html" value="any" checked> <span>any</span> </label> <label class="radio"> <input type="radio" name="closedby-demo-html" value="closerequest"> <span>closerequest</span> </label> <label class="radio"> <input type="radio" name="closedby-demo-html" value="none"> <span>none</span> </label> </div> </div> </div> <div class="actions"> <button commandfor="closing-behaviors-dialog-html" command="close" class="button"> Close manually </button> </div></dialog>
<script> const radios = document.querySelectorAll('input[name="closedby-demo-html"]')
radios.forEach((radio) => { radio.addEventListener("change", () => { const dialog = radio.closest('.example-preview')?.querySelector("#closing-behaviors-dialog-html") if (dialog instanceof HTMLDialogElement) { dialog.setAttribute("closedby", radio.value) } }) })</script>Accessibility
-
The
tabindexattribute must not be used on the<dialog>element.
Role & attributes
| Role/attribute | Usage |
|---|---|
role="dialog" | Identifies the element that serves as the dialog container. |
role="alertdialog" | If the dialog is a confirmation window communicating an important message that requires a confirmation or other user response. |
aria-labelledby="IDREF" | Gives the dialog an accessible name by referring to the element that provides the dialog title. |
aria-describedby="IDREF" | Gives the dialog an accessible description by referring to the dialog content that describes the primary message or purpose of the dialog. |
aria-modal="true" | Tells assistive technologies that the windows underneath the current dialog are not available for interaction (inert). |
Keyboard support
| Key | Function |
|---|---|
| Tab |
|
| Shift + Tab |
|
| Esc | Closes the dialog. |
API
| Type | Modifiers | Default | Description |
|---|---|---|---|
| Styles | .dialog.card.elevated | Included | The dialog uses card styles by default. |
| Closed by | closedby attribute | - | The attribute used to control close behavior. |
Browser support
Full support Supported since v135. Partial support
Missing:
container-style-queries, overlay.
Partial support
Missing: dialog-closedby, overlay.
See also the full browser support guide.
Installation
Dependencies
@layer components.extended { :where(.dialog) { inline-size: 100%; inset: 0; margin: auto; margin-block-start: 15%; max-inline-size: calc(100% - var(--size-4)); padding-block: 0; pointer-events: none; position: fixed;
@media (width > 600px) { max-inline-size: 60ch; }
/* Animation */ /* There's no close animation, intentionally */ opacity: 0;
&::backdrop { backdrop-filter: blur(1px); background-color: rgba(0, 0, 0, 0.5);
@media (prefers-reduced-motion: reduce) { backdrop-filter: none; } }
&:not([open]) { display: none; }
&[open] { pointer-events: all; }
.actions { justify-content: end; padding-inline: var(--size-3) var(--size-1); }
&[open] { opacity: 1; transition: display 0.2s allow-discrete, overlay 0.2s allow-discrete, opacity 0.2s var(--ease-out-1);
@starting-style { opacity: 0; } }
@media (prefers-reduced-motion: no-preference) { &[open] { margin-block-start: 15%;
@starting-style { opacity: 0; } } } }
:where(html:has(.dialog[open])) { block-size: 100%; overflow: hidden; }}@layer components.root { :where(.card) { --_bg-tonal: var(--surface-tonal); --_bg-elevated: var(--surface-elevated); --_bg-surface: var(--surface-default); --_border-color: var(--border-color); --_card-bg-color: var(--_bg-surface); --_card-border-color: transparent; --_card-border-width: 0;
--_card-shadow: none; --_shadow-light: var(--shadow-3); --_shadow-dark: var(--shadow-4); --_shadow-elevated: var(--_shadow-light);
@container style(--color-scheme: dark) { --_shadow-elevated: var(--_shadow-dark); }
background-color: var(--_card-bg-color); border-color: var(--_card-border-color); border-radius: var(--border-radius); border-style: solid; border-width: var(--_card-border-width); box-shadow: var(--_card-shadow); display: flex; flex-direction: column; gap: var(--size-3); overflow: hidden; padding-inline: 0; position: relative;
/* Variants */ &.text { --_card-bg-color: transparent; --_card-border-color: transparent; --_card-border-width: 0; --_card-shadow: none; }
&.tonal { --_card-bg-color: var(--_bg-tonal); --_card-border-width: 1px; }
&.elevated { --_card-bg-color: var(--_bg-elevated); --_card-shadow: var(--_shadow-elevated); }
&.outlined { --_card-bg-color: var(--_bg-surface); --_card-border-color: var(--_border-color); --_card-border-width: 1px; }
&> :where(hgroup, .content) { padding-inline: var(--size-3);
&:last-child { padding-block-end: var(--size-3); } }
/* Header */ &>hgroup { padding-block: var(--size-3) 0;
/* Top paragraph */ &>p:first-of-type:first-child { line-height: 1.3; }
:where(h1, h2, h3, h4, h5, h6):last-child { margin-block-end: 0; }
/* Bottom paragraph */ &>p:last-of-type:last-child:not(:first-child) { font-size: var(--font-size-1); } }
/* Content */ &>.content:where(:only-child, :first-child) { padding-block: var(--size-3) var(--size-4); }
/* Actions */ &>.actions { display: flex; gap: var(--size-2); margin-block: var(--size-2) 0; padding-block-end: var(--size-2); padding-inline: var(--size-3);
&:has(.button:first-child[class="button"]) { padding-inline: var(--size-1) var(--size-3); }
&:has(.button:not([class="button"])) { padding-block-end: var(--size-2); }
/* Alignment */ &.align-end { justify-content: end;
&:has(.button:first-child[class="button"]) { padding-inline: var(--size-3) var(--size-1); } } }
}}