Skip to main content

Core · Feedback and status

Modal

Asks for a decision that must be made before the reader goes on, in a native dialog.

Example

A button that opens a dialog asking whether to delete a draft.

Delete this draft?

You cannot undo this. The draft and its comments are deleted.

A native <dialog>.

Escape closes it and focus goes back to the button that opened it. Nothing is deleted here.

Known issues

Some supported browsers do not have the command and commandfor attributes yet. In those browsers, with JavaScript off, the open button does nothing.

Give the reader another way to the same action.

Code

Copy the markup

A bare dialog needs no class: it has a surface and is centred.

HTML
<button type="button" commandfor="rename" command="show-modal">Rename project</button>

<dialog id="rename" aria-labelledby="rename-title">
  <h2 id="rename-title">Rename project</h2>
  <form method="dialog">
    <label for="rename-name">New name</label>
    <input id="rename-name" type="text" autofocus>
    <button type="button" commandfor="rename" command="close">Cancel</button>
    <button value="rename">Rename project</button>
  </form>
</dialog>

.luz-modal adds the header with its close button, the body and the footer, the small-screen layout and the fade. With the class:

HTML
<button class="luz-btn luz-btn--primary" type="button" commandfor="rename" command="show-modal">Rename project</button>

<dialog class="luz-modal" id="rename" aria-labelledby="rename-title">
  <div class="luz-modal__header">
    <button class="luz-modal__close" type="button" commandfor="rename" command="close" aria-label="Close dialog">×</button>
    <h2 class="luz-modal__title" id="rename-title">Rename project</h2>
  </div>
  <form method="dialog">
    <div class="luz-modal__body">
      <div class="luz-field">
        <label class="luz-field__label" for="rename-name">New name</label>
        <input class="luz-input" id="rename-name" type="text" autofocus>
      </div>
    </div>
    <div class="luz-modal__footer">
      <button class="luz-btn luz-btn--ghost" type="button" commandfor="rename" command="close">Cancel</button>
      <button class="luz-btn luz-btn--primary" value="rename">Rename project</button>
    </div>
  </form>
</dialog>
JS
import { initModals } from "@luz-ds/core/modal";
initModals();

Variants and options

One size and no modifiers: up to 600 px wide and 90 % of the viewport high, scrolling inside when the content is longer. Up to 640 px wide, or under 30 rem tall, it fills the screen.

Class or attributeWhat it does
.luz-modal
The dialog surface, with a dimmed and slightly blurred backdrop
.luz-modal__header, __title
The title row, with a rule under it
.luz-modal__close
The × button, at the end of the title row, 28 px at least (44 px when the modal fills the screen)
.luz-modal__body
The content
.luz-modal__footer
The buttons, at the end of the row, with a rule above them
command="show-modal", command="close"
Open and close the dialog named in commandfor

How it works

A native <dialog>, styled by components/modal.css and opened as a modal.

  • The browser does the hard part: the page behind is inert, focus stays inside, Escape closes, and focus goes back to the opener.
  • Buttons open and close it with command and commandfor, with no script.
  • initModals() from modal.js handles the same two commands in browsers that do not support them yet, and does nothing in the others. luz.js runs it.
  • A <form method="dialog"> inside closes the dialog when it is sent, and its returnValue is the value of the button pressed.
  • Up to 640 px wide, or under 30 rem tall (a phone held sideways), it fills the screen: no margin, radius or shadow.
  • Then it is the one thing that scrolls: the header with the close stays on top, the footer at the bottom (it scrolls in a short window). The padding keeps clear of the safe-area insets.

Without JavaScript

In browsers that support command, everything works. In the others the open button does nothing: see Known issues.

Keyboard and ARIA

Tab and Shift+Tab move inside the dialog only; Escape closes it.

  • On open, focus goes to the first focusable element, or to the one with autofocus.
  • Name the dialog with aria-labelledby pointing at its title.
  • The × button needs aria-label="Close dialog": its text is a symbol.

Contrast and focus are in the Design tab.

Design

When to use

Use a modal to ask for a decision or a short input that must be settled before the reader goes on.

  • To confirm an action that cannot be undone.
  • To ask for one or two values, such as a new name.

When not to use

Do not use a modal for something the reader can ignore or come back to.

  • For information about the page, use an alert.
  • For a confirmation that needs no answer, use a toast.
  • For a long form, use its own page.
  • Never open a modal when the page loads.

Do and don't

Do The title asks the question and the main button repeats the action.
Don't “Are you sure?” with “Cancel” and “Continue”: the reader has to read the body to know what “Continue” does.

Writing the content

The title asks the question or names the task: "Delete this draft?"

  • The body says what happens, in one or two sentences.
  • The main button repeats the action: "Delete draft". The other one is "Cancel".
  • Never "OK", "Yes" or "Confirm".

Contrast and focus

Text sits on the dialog's surface, above a scrim that dims the page.

PartColour on backgroundLightDarkHigh contrastNeeds
Title and body
--luz-text-primary on --luz-surface
16.5:1
14.0:1
21.0:1
4.5:1
Close button
--luz-text-secondary on --luz-surface
6.4:1
6.9:1
19.3:1
4.5:1
Focus ring
--luz-focus-ring on --luz-surface
5.9:1
7.2:1
21.0:1
3:1

Measured from the token values of each mode, rounded down. Needs is the WCAG 2.2 AA minimum: 4.5:1 for text, 3:1 for focus rings, borders and icons.

  • Focus: it moves into the dialog when it opens and back to the button that opened it when it closes. The close button has a 2 px --luz-focus-ring outline, 2 px outside.
  • Edges: the dialog has a border and a shadow, so it stays apart from the page in forced colors too.
  • Target: the close button is at least 28 px square, and 44 px when the modal fills the screen.

Tokens it uses

The 22 tokens that components/modal.css reads, by family. Each family links to its page, with the values in every mode. In Figma they are variables with the same names: see Tokens in Figma.

Spacing and sizing
--luz-control-lg, --luz-control-sm, --luz-space-2xs, --luz-space-lg, --luz-space-md, --luz-space-sm, --luz-space-xl, --luz-space-xs
Typography
--luz-text-2xl, --luz-text-lg
Radius
--luz-radius-md
Shadows
--luz-shadow-lg
Motion
--luz-duration-base, --luz-duration-fast, --luz-easing-enter

Keyboard and ARIA are in the Code tab.