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.
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.
<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:
<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>
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 attribute | What it does |
|---|---|
.luz-modal | The dialog surface, with a dimmed and slightly blurred backdrop |
.luz-modal, __title | The title row, with a rule under it |
.luz-modal | The × button, at the end of the title row, 28 px at least (44 px when the modal fills the screen) |
.luz-modal | The content |
.luz-modal | 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
commandandcommandfor, with no script. initModals()frommodal.jshandles the same two commands in browsers that do not support them yet, and does nothing in the others.luz.jsruns it.- A
<form method="dialog">inside closes the dialog when it is sent, and itsreturnValueis 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-labelledbypointing 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
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.
| Part | Colour on background | Light | Dark | High contrast | Needs |
|---|---|---|---|---|---|
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-ringoutline, 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.
- Color
--luz-divider,--luz-focus-ring,--luz-scrim,--luz-surface,--luz-surface-hover,--luz-text-primary,--luz-text-secondary- 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.