Skip to content

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.

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

  • Snackbar: informative but non-interruptive

Javascript is required

In order to toggle a <dialog> you will need to use Javascript.

Are you sure?

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sodales, nulla sit amet porttitor rhoncus.
html
<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>
js
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();
});

Accessibility

  • The tabindex attribute must not be used on the <dialog> element.

Role & attributes

Role/attributeUsage
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

KeyFunction
Tab
  • Moves focus to next focusable element inside the dialog.
  • When focus is on the last focusable element in the dialog, moves focus to the first focusable element in the dialog.
Shift + Tab
  • Moves focus to previous focusable element inside the dialog.
  • When focus is on the first focusable element in the dialog, moves focus to the last focusable element in the dialog.
EscCloses the dialog.

Source: w3.org, MDN

Basic example

html
<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>

Anatomy

  1. <dialog> container
Dialog
html
<dialog>
  <!--  -->
</dialog>

API

A <dialog> element on its own doesn't do much. It's recommended to use it in combination with the card component.

Browser compatibility

Installation

css
: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;
}
css
: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(--surface-border-radius, 0.25rem);
  border-style: solid;
  border-width: var(--_border-width);
  box-shadow: var(--_shadow);
  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 {
    margin-block: var(--size-3);
    & > * {
      margin: 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 {
    margin-block: var(--size-3) var(--size-4, 1.25rem);
  }

  .actions {
    display: flex;
    gap: var(--size-1);
    margin-block: var(--size-4, 1.25rem) 0;
    padding-block-end: var(--size-1);
    padding-inline: var(--size-1) var(--size-3);
  }
}