busy-office-ui
Menu

Drawers (off-canvas)

A panel that slides in over the screen: navigation on narrow shells (start side) and filters or record previews on wide ones (--end). It is a native <dialog> variant, so the focus trap, Esc, backdrop and focus-return come from the platform. Not when the panel's content must stay visible beside the record — sliding over the screen hides the context the user is comparing against; use master-detail for that.

Open the drawer — in its own document

An iframe, so the drawer takes over nothing but itself. .bo-offcanvas is a real <dialog>: opened inline it renders in the page's top layer and covers the documentation around it, which makes the one control you want to study the thing hiding everything else. In its own document the drawer still gets a real top layer, a real backdrop and real Esc handling — the platform behaviour is the point — but scoped to the frame.

Start-side drawer — navigation on a narrow shell

busy-office

Markup

<button class="bo-btn bo-btn--secondary"
        data-dialog-trigger="drawer-nav" aria-label="Open navigation">☰ Menu</button>

<dialog class="bo-offcanvas" id="drawer-nav" data-state="closed"
        aria-labelledby="drawer-title">
  <div class="bo-offcanvas__header">
    <span id="drawer-title" class="bo-navbar__brand">busy-office</span>
    <form method="dialog">
      <button class="bo-btn bo-btn--ghost bo-btn--icon" aria-label="Close">✕</button>
    </form>
  </div>
  <nav class="bo-sidebar-nav" aria-label="Main">…same markup as the sidebar…</nav>
</dialog>
<!-- Opens via initDialogs() — same wiring as any dialog: backdrop, Esc,
     focus-return and Tab loop are all inherited. -->

End-side drawer — filters & detail panels

Filters

--end slides the drawer in from the end edge — the home for filter panels and record previews (start side stays the navigation convention). Same dialog wiring, mirrored slide animation, RTL-aware.

Which side, and when

  • Start side — navigation, because that is where the sidebar lives; a drawer that replaces the rail should come from where the rail was.
  • --end — filters, detail previews, anything that supplements the screen you are on rather than taking you elsewhere.
  • Both mirror correctly under dir="rtl": "start" and "end" are logical, not left and right.

Quick reference generated

ClassRole
.bo-offcanvas block (root)
.bo-offcanvas--end variant: end
.bo-offcanvas__header part: header
.bo-sidebar-nav block (root)
.bo-sidebar-nav__heading part: heading
.bo-sidebar-nav__label part: label
.bo-sidebar-nav__link part: link

State attributes: data-state

API generated from CSS

Blocks bo-offcanvasbo-sidebar-nav
Parts bo-offcanvas__headerbo-sidebar-nav__headingbo-sidebar-nav__labelbo-sidebar-nav__link
Variants bo-offcanvas--end
data-* hooks data-state="open"
Component tokens --bo-offcanvas-offset
JS required REQUIRED: initDialogs() — a drawer is a native dialog and never opens without it. It is the same one call as any other dialog, so backdrop, Esc, focus-return and the Tab loop are inherited rather than reimplemented.
  • It is a <dialog>: initDialogs() wires it exactly like any other — backdrop, Esc, focus-return and the Tab loop are inherited, not reimplemented.
  • Give it an accessible name with aria-labelledby pointing at the header title.
  • A drawer is modal: while it is open the page behind is inert. For a panel that must stay open alongside the content, use a layout column, not a drawer.
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-surface 10.31 11.46
text-muted bg-surface 7.56 6.65
bg-surface text-primary 17.74 16.15
danger-text bg-surface 6.47 8.89
success-text bg-surface 5.02 12.02
accent bg-surface 5.47 9.07
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-surface 4.83 6.65
--bo-state-error-color bg-surface 4.83 6.1
warning-strong bg-surface 5.02 7.86
text-primary bg-muted 16.12 13.75

Maturity generated

Introduced
v0.3.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.3.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 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 min(18rem, 85vw) explains both halves — wider than the 14rem rail it contains, and capped so a strip of page always shows as the dismiss affordance (94.6b)
Interaction 3 / 3 reuses the dialog wiring wholesale (backdrop, Esc, focus-return, Tab loop); translate is physical so RTL gets explicit animation overrides
Content 3 / 3 the opener names the case it must not take — content that has to stay visible beside the record — and sends it to master-detail (94.8)
Fit 3 / 3 the drawer-nav pattern — a sidebar-nav inside it at phone width; used in 1 pattern screen
↑↓ move Enter open Esc close