busy-office-ui
Menu

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.

Approve purchase order

Approve PO-88213 for $12,400.00?

Wide — for tables and two-column forms

PO-88213 — line items

ItemQtyUnitTotal
Standing desk4$1,208.00$4,832.00
Monitor arm8$92.50$740.00
Cable tray12$18.90$226.80

--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

Reject PO-88213?

The requester will be notified and the PO returns to draft. This can't be undone from this screen.

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

ClassRole
.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-labelledby pointing at the title is YOUR markup; backdrop, Esc, and focus-return come free from native showModal().
  • The Tab focus loop and data-state animation 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)
TextonLightDark
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
↑↓ move Enter open Esc close