Skip to content

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.

Usage

Non-modal

  • Snackbar: informative but non-interruptive

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
@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;
  }
}
css
@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);
    }
  }
}