Dialog
Native <dialog> + initDialogs(): browser handles
backdrop, ESC, and focus return; the behavior adds trigger wiring, a Tab loop, and
data-state for the enter animation.
Not for telling the user something — a modal takes the
screen and demands a decision, so it is for a choice that must be made
now. Report an outcome with an
alert or toast instead.
Wide — for tables and two-column forms
--wide raises the max width for content
that genuinely needs it — an embedded table (compact density pairs well here) or
a two-column form. Default width is right for confirmations; don't reach for
wide by default.
Destructive confirm — no accidental dismiss
Clicking the backdrop closes a dialog by
default (light dismiss). For destructive or must-choose flows, opt out
with data-dismissible="false" — Esc and the explicit buttons still
work, only the accidental backdrop click is disarmed. Pair the destructive
action with .bo-btn--danger and lead with the safe choice.
Markup — the canonical dialog recipe
<dialog class="bo-dialog" id="approve-dialog"
aria-labelledby="approve-title" data-state="closed">
<form method="dialog">
<header class="bo-dialog__header">
<h2 class="bo-dialog__title" id="approve-title">Approve purchase order</h2>
<button class="bo-btn bo-btn--ghost bo-btn--icon" value="cancel"
aria-label="Close">✕</button>
</header>
<div class="bo-dialog__body">…</div>
<footer class="bo-dialog__footer">
<button class="bo-btn bo-btn--secondary" value="cancel">Cancel</button>
<button class="bo-btn" value="confirm">Approve</button>
</footer>
</form>
</dialog>
<button class="bo-btn" data-dialog-trigger="approve-dialog">Approve PO…</button>
<script type="module">
import { initDialogs } from "@busy-office/ui/js";
initDialogs(); // once, ever — event delegation; HTMX swaps need no re-init
</script> Quick reference generated
| Class | Role |
|---|---|
| .bo-dialog | block (root) |
| .bo-dialog--wide | variant: wide |
| .bo-dialog__body | part: body |
| .bo-dialog__footer | part: footer |
| .bo-dialog__header | part: header |
| .bo-dialog__title | part: title |
State attributes: data-dialog-triggerdata-dismissibledata-state
API generated from CSS
| Blocks | bo-dialog |
|---|---|
| Parts | bo-dialog__bodybo-dialog__footerbo-dialog__headerbo-dialog__title |
| Variants | bo-dialog--wide |
| data-* hooks | data-dialog-triggerdata-dismissibledata-state="open" |
| JS required | REQUIRED: initDialogs() — dialogs never open without it. Call once; delegation survives all swaps. |
- ARIA contract:
aria-labelledbypointing at the title is YOUR markup; backdrop, Esc, and focus-return come free from nativeshowModal(). - The Tab focus loop and
data-stateanimation sync are supplied by the behavior. - Toasts cannot float above an open modal (top layer) — render confirmations inside the dialog.
Contrast — 15 AA pairs this component uses (generated, CI-gated)
| Text | on | Light | Dark |
|---|---|---|---|
| text-primary | bg-surface | 17.74 | 16.15 |
| text-primary | bg-canvas | 16.98 | 18.08 |
| text-secondary | bg-muted | 9.37 | 9.75 |
| text-muted | bg-muted | 6.87 | 5.66 |
| bg-surface | text-primary | 17.74 | 16.15 |
| text-primary | bg-hover | 16.12 | 13.75 |
| text-primary | bg-selected | 17.01 | 11.86 |
| text-primary | bg-surface-raised | 17.74 | 14.51 |
| border-control | bg-muted | 4.39 | 5.66 |
| accent | bg-muted | 4.97 | 7.72 |
| warning-strong | bg-muted | 4.56 | 6.69 |
| danger | bg-muted | 4.39 | 5.19 |
| text-primary | bg-muted | 16.12 | 13.75 |
| success-text | bg-muted | 4.56 | 10.23 |
| danger-text | bg-muted | 5.88 | 7.57 |
Maturity generated
- Introduced
- v0.1.0
- CSS floor
- Chrome/Edge 117 · Firefox 129 · Safari 17.5
- Alignment scored
- 2026-08-21
- Assistive-tech evidence
- None recorded
Pin this component's first release with npm i @busy-office/ui@0.1.0.
The floor is derived from this component's stylesheet alone — set by
@starting-style — so it sits at or below the
whole framework's Chrome/Edge 119 · Firefox 129 · Safari 17.5.
No assistive-technology runtime result is recorded for any component: it needs a human listening to VoiceOver and NVDA on owner hardware; no gate in this repo can simulate announcement behaviour.
What automation covers instead is test:axe across every page at 1440 and 390, and check:forced-colors — see
accessibility.
Design-system alignment generated
100% (18/18) — how closely this component follows the design language. Each rule is scored 0-3 against a checkable citation; rules that don't apply leave the denominator. Scored 2026-08-21.
| Rule | Score | Evidence |
|---|---|---|
| Typography | 3 / 3 | --bo-font-size-md on the title; no raw font-size |
| Colour | 3 / 3 | ::backdrop now consumes --bo-color-scrim, the shared token added in the 94.6a sweep; zero raw colour, and the scrim is deliberately not theme-remapped (a scrim pushes the page back rather than tinting a surface) |
| Spacing | 3 / 3 | the 32rem measure, the 56rem--wide opt-in and the 0.5rem entry travel all carry their reason, incl. why the travel must not read like offcanvas sliding in (94.6b) |
| Interaction | 3 / 3 | native <dialog>: backdrop, Esc and focus-return come from the platform; behaviors/dialog.ts adds only trigger wiring and data-state sync |
| Content | 3 / 3 | opener carries the wrong-choice clause: "Not for telling the user something" — a modal takes the screen and demands a decision now, so the clause names and links an alert or toast for reporting an outcome |
| Fit | 3 / 3 | the modal context; po-app uses it at 13 sites, the heaviest real usage in this family |