Modal
Usage
Use a modal when the dispatcher must finish or cancel a short task before going on: reassign a mechanic, edit a service interval, confirm a deletion. The rest of the page becomes inert until it closes.
- Modal dialog — a focused task with a form or a decision. Keep it to one screen of content; long flows belong on a page.
- Alert dialog — a confirmation that interrupts: destructive or irreversible actions only ("Delete work order WO-1042?").
- Don't use a modal for information the user doesn't have to act on (use an alert or toast), for filters (use a drawer or popover), or open a modal from a modal.
Content: the title states the task or the consequence as a question ("Reassign work order WO-1042", "Delete work order WO-1042?"). Buttons are verb + object ("Delete work order", "Reassign mechanic"); the dismiss action is "Cancel", never "No" or "OK". Put the primary action at the inline end with Cancel beside it.
Anatomy
- Backdrop —
::backdropwith--color-bg-overlay - Container —
<dialog class="modal">on the overlay surface,--overlay-radiusand--overlay-shadow - Header —
.modal__header: title.modal__title(anh2) and close button.modal__close; alert dialogs show a tone icon.modal__iconinstead of the close button - Body —
.modal__body, the only part that scrolls - Footer —
.modal__footer: Cancel, then the primary action at the inline end
Examples
Open dialogs are previewed in the page with the docs-only .is-preview class; every demo also has a real trigger that opens the dialog in the top layer with commandfor/command="show-modal", without JavaScript.
Modal dialog
Closed: only the trigger is in the page. The dialog is display: none until opened.
<button type="button" class="btn btn--secondary" commandfor="dlg-reassign" command="show-modal">Reassign mechanic</button>
<dialog id="dlg-reassign" class="modal" aria-labelledby="reassign-title" aria-describedby="reassign-desc" closedby="any">
<div class="modal__header">
<h2 class="modal__title" id="reassign-title">Reassign work order WO-1042</h2>
<button type="button" class="btn btn--ghost btn--sm modal__close" commandfor="dlg-reassign" command="close" aria-label="Close">
<svg class="btn__icon" aria-hidden="true" focusable="false"><use href="#icon-x"></use></svg>
</button>
</div>
<div class="modal__body"><p id="reassign-desc">…</p></div>
<div class="modal__footer">
<button type="button" class="btn btn--ghost" commandfor="dlg-reassign" command="close">Cancel</button>
<button type="button" class="btn btn--primary">Reassign mechanic</button>
</div>
</dialog>
<!-- Sizes and variants -->
<dialog class="modal modal--sm">… <dialog class="modal modal--lg">…
<dialog class="modal modal--scroll">… <dialog class="modal modal--fullscreen-mobile">…
Alert dialog
<button type="button" class="btn btn--destructive" commandfor="dlg-delete" command="show-modal">Delete work order</button>
<dialog id="dlg-delete" class="modal modal--alert modal--danger" role="alertdialog"
aria-labelledby="delete-title" aria-describedby="delete-desc">
<div class="modal__header">
<span class="modal__icon" aria-hidden="true"><svg focusable="false"><use href="#icon-trash"></use></svg></span>
<h2 class="modal__title" id="delete-title">Delete work order WO-1042?</h2>
</div>
<div class="modal__body"><p id="delete-desc">… This can't be undone.</p></div>
<div class="modal__footer">
<button type="button" class="btn btn--ghost" commandfor="dlg-delete" command="close" autofocus>Cancel</button>
<button type="submit" class="btn btn--destructive" form="delete-wo-1042">Delete work order</button>
</div>
</dialog>
API
| Hook | Values | Purpose |
|---|---|---|
<dialog class="modal"> | block | Modal surface; open with command="show-modal" |
.modal--sm | --lg | modifier | Width 30rem / 48rem (md 40rem default), always within the viewport |
.modal--scroll | modifier | Adds separators above and below a long scrolling body |
.modal--fullscreen-mobile | modifier | Fills the screen below 40rem, stacks footer actions |
.modal--alert + .modal--danger | --warning | modifier | Alert dialog layout and icon tone; pair with role="alertdialog" |
.modal__header | __title | __close | __icon | __body | __footer | parts | See anatomy |
commandfor="id" command="show-modal | close" | invoker attributes | Open and close without JavaScript |
closedby="any" | attribute | Light dismiss (backdrop click) for non-destructive, non-form dialogs only; enhancement |
autofocus | attribute | Initial focus target; on alert dialogs put it on Cancel |
.is-preview | .is-mobile | docs-only class | Render an open dialog in the page flow / force the mobile layout |
Do and don't
Accessibility
| Key | Behavior |
|---|---|
| Tab / Shift+Tab | Moves between focusable elements inside the dialog; the page behind is inert |
| Esc | Closes the dialog (on an alert dialog it equals Cancel) |
| Enter / Space | Activates the focused button |
- Role: native
<dialog>opened withshowModal()(via the invoker) is a modal dialog — top layer, inert background, Esc handling. Alert dialogs addrole="alertdialog"(APG Alert Dialog). - Name and description:
aria-labelledby→ the title;aria-describedby→ the main paragraph, read on open. - Focus: moves to the first focusable element or the
autofocusone (Cancel in alert dialogs, the least destructive action). On close the browser returns focus to the invoker. - No light dismiss on alert dialogs or dialogs with forms: an accidental click must not lose input.
- The scrolling body is focusable (
tabindex="0"+aria-label) so keyboard users can scroll it. - Fallback: browsers without invoker commands need the shared invoker polyfill that maps
commandtoshowModal()/close(). - Forced colors: the dialog keeps a
CanvasTextborder.
Tokens
| Custom property | Purpose |
|---|---|
--color-elevation-surface-overlay | Dialog surface |
--overlay-radius, --overlay-shadow | Corner radius and elevation |
--color-bg-overlay | Backdrop scrim |
--size-container-sm | -md | Widths (sm = 0.75 × container-sm) |
--color-feedback-{danger|warning|info}-{bg|icon} | Alert dialog icon tone |
--typography-h4-* | Title |
--motion-duration-base | -fast, --motion-easing-enter | -exit | Enter and exit |
--size-touch-target | Full-width footer buttons on mobile |