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.
Components
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.
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.
In order to toggle a <dialog>
you will need to use Javascript.
<dialog class="card elevated">
<hgroup>
<h2 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 class="button">Cancel</button>
<button class="button">Save</button>
</div>
</dialog>
<button id="open-dialog-button">Open dialog</button>
const dialog = document.querySelector("dialog")
const showButton = document.getElementById("open-dialog-button")
const closeButton = document.querySelector("dialog button")
showButton.addEventListener("click", () => {
dialog.showModal()
})
closeButton.addEventListener("click", () => {
dialog.close()
})
tabindex
attribute must not be used on the <dialog>
element.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). |
Key | Function |
---|---|
Tab |
|
Shift + Tab |
|
Esc | Closes the dialog. |
<dialog
role="alertdialog"
aria-labelledby="dialog-heading"
aria-describedby="dialog-content"
aria-modal="true"
>
<div class="card">
<hgroup>
<h2 id="dialog-heading" class="h4">Dialog heading</h2>
</hgroup>
<div id="dialog-content" class="content"><!-- --></div>
<div class="actions">
<!-- -->
</div>
</div>
</dialog>
<dialog>
container<dialog>
<!-- -->
</dialog>
A <dialog>
element on its own doesn't do much. It's recommended to use it in combination with the card component.
@layer components.has-deps {
:where(dialog) {
margin-block-start: 15%; /* vertical alignment */
padding-block: 0;
pointer-events: none;
&::backdrop {
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
@media (prefers-reduced-motion: reduce) {
backdrop-filter: none;
}
}
&[open] {
pointer-events: all;
}
.actions {
justify-content: end;
padding-inline: var(--size-3) var(--size-1);
}
/* Animation */
/* There's no close animation, intentionally */
opacity: 0;
&[open] {
opacity: 1;
transition:
display 0.2s allow-discrete,
margin-block-start 0.3s var(--ease-1),
overlay 0.2s allow-discrete,
opacity 0.2s var(--ease-out-1);
@starting-style {
opacity: 0;
}
}
@media (prefers-reduced-motion: no-preference) {
margin-block-start: 17%;
&[open] {
margin-block-start: 15%;
@starting-style {
margin-block-start: 17%;
}
}
}
}
:where(html:has(dialog[open])) {
overflow: hidden;
}
}
@layer components.base {
:where(.card) {
--_bg-color: transparent;
--_border-color: transparent;
--_border-width: 0;
--_shadow: none;
background-color: var(--_bg-color);
border-color: var(--_border-color);
border-radius: var(--border-radius, 0.25rem);
border-style: solid;
border-width: var(--_border-width);
box-shadow: var(--_shadow);
display: flex;
flex-direction: column;
gap: var(--size-3);
overflow: hidden;
padding-inline: 0;
/* Variants */
&.text {
--_bg-color: transparent;
--_border-color: transparent;
--_border-width: 0;
--_shadow: none;
padding-inline: 0;
}
&.tonal {
--_bg-color: var(--surface-tonal);
--_border-width: 1px;
}
&.elevated {
--_bg-color: var(--surface-elevated);
--_border-color: transparent;
--_border-width: 0;
--_shadow: var(--shadow-3);
/* Adjust shadow in dark mode */
@container style(--color-scheme: dark) {
--_shadow: var(--shadow-4);
}
}
&.outlined {
--_bg-color: var(--surface-default);
--_border-color: var(--border-color);
--_border-width: 1px;
}
& > :where(hgroup, .content) {
padding-inline: var(--size-3);
}
& > hgroup {
padding-block: var(--size-3) 0;
/* Top paragraph */
& > p:first-of-type:first-child {
line-height: 1.3;
}
/* Bottom paragraph */
& > p:last-of-type:last-child:not(:first-child) {
font-size: var(--font-size-1, 1rem);
}
}
& > .content:where(:only-child, :first-child) {
padding-block: var(--size-3) var(--size-4);
}
& > .actions {
display: flex;
gap: var(--size-1);
margin-block: var(--size-2) 0;
padding-block-end: var(--size-1);
padding-inline: var(--size-1) var(--size-3);
}
}
}