busy-office-ui
Menu

Sidebar navigation

The module rail of an ERP — grouped links that collapse to icons when the shell is narrow, not when the viewport is. This page's own sidebar is the live demo; the embedded shell below proves the container-query behaviour independently. Not for navigating within one screen — this rail moves the reader between destinations, and every entry is somewhere else. A long record's own sections are not destinations: jumping to "Delivery" should scroll, not navigate, and marking it aria-current="page" would claim a page change that never happened. That is object page's anchor bar, or plain tabs when the sections are mutually exclusive.

Both rail states, in real shells

Shell wider than 56rem — labels visible.

Shell at 56rem or narrower — collapsed to icons.

These are iframes, and they have to be. The rail collapses under @container bo-shell (max-width: 56rem), so it answers to the width of its shell, not the viewport. A demo inline on this page has no such container, so it rendered the expanded rail at every width — including 390 — and the state the component is most interesting for could not be shown at all. A frame gives the demo its own document and its own container, so the two panes above differ by nothing but how wide their shell is. Drag the browser narrow and they stay as they are; that is the point.

Grouped sections in an embedded shell

Embedded app

Sections (__section + __heading) group a real ERP's modules — headings hide with the labels when this 700px shell collapses to icons. Container queries, not media queries.

How many entries fit before the top group scrolls away. The rail is one scrolling column, so a long second group pushes the first out of view — and the first group is usually the one you need from anywhere (the modules). Measured on a real suite shell with 7 module entries: at a 900px-tall viewport the modules stay visible up to 11 entries in the second group and are gone by 17; on a 700px laptop the limit is 11. If a group needs more than that, the answer is not a taller rail — move the long list into the page (a tab strip or an anchor bar belongs to the screen, not the chrome), or split the module. A second, narrower icon rail is what large suites use, and this framework deliberately does not ship one yet: no screen here has hit the limit, and adding a column to .bo-app-shell — the most-composed layout in the framework — on a hypothetical is how a grid grows a slot nobody fills (roadmap 130.2, GAP-1).

A label longer than the rail

A label wider than the rail wraps onto another line. It is not truncated and it does not widen the rail. Wrapping is the deliberate choice: an ellipsis hides the part of a name that distinguishes it — "Accessibility Conformance…" and "Accessibility Conformance Report" read alike — and letting the label push the rail wider puts a horizontal scrollbar inside the navigation, so a reader has to drag sideways to finish reading a link. Both were real: this rail spilled 15.7px and scrolled until it was fixed. Nothing here is configurable, so long module names are safe to use without measuring them first.

On a narrow screen you are seeing icons, not wrapped text, and that is the same rule as above rather than an exception to it: this demo sits inside the docs' own app shell, so when that shell is narrow the rail collapses to icon-only and every label is visually hidden — wrapping only applies where there are labels to wrap. Widen the window to see it.

Markup

<nav class="bo-sidebar-nav bo-app-shell__sidebar" aria-label="Main">
  <div class="bo-sidebar-nav__section">
    <div class="bo-sidebar-nav__heading">Purchasing</div>
    <ul>
      <li><a class="bo-sidebar-nav__link" href="/invoices" aria-current="page">
        <span class="bo-icon bo-icon--invoice bo-sidebar-nav__icon" aria-hidden="true"></span>
        <span class="bo-sidebar-nav__label">Invoices</span>
      </a></li>
    </ul>
  </div>
</nav>

Quick reference generated

ClassRole
.bo-sidebar-nav block (root)
.bo-sidebar-nav__heading part: heading
.bo-sidebar-nav__icon part: icon
.bo-sidebar-nav__label part: label
.bo-sidebar-nav__link part: link
.bo-sidebar-nav__section part: section

State attributes: aria-current

API generated from CSS

Blocks bo-sidebar-nav
Parts bo-sidebar-nav__headingbo-sidebar-nav__iconbo-sidebar-nav__labelbo-sidebar-nav__linkbo-sidebar-nav__section
ARIA styled aria-current
JS required None — CSS-only.
  • The current page carries aria-current="page" — that is the programmatic channel; the visual highlight is the other half.
  • Icons are aria-hidden and labels stay in the accessibility tree when the rail collapses, so a screen-reader user never loses the link text.
  • Collapse is driven by a named container query on the shell — the same sidebar behaves correctly inside a split view, which a media query cannot do.
  • Sections (__section + __heading) group modules; headings hide with the labels when the rail collapses.
Contrast — 22 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-secondary bg-muted 9.37 9.75
text-muted bg-surface 7.56 6.65
text-muted bg-muted 6.87 5.66
bg-surface text-primary 17.74 16.15
accent-text accent-subtle 7.27 8.38
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-secondary bg-selected 9.88 8.41
accent-text bg-selected 7.27 8.38
text-primary bg-surface-raised 17.74 14.51
text-secondary bg-canvas 9.86 12.82
border-control bg-surface 4.83 6.65
--bo-state-error-color bg-surface 4.83 6.1
accent accent-subtle 5.25 6.66
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 105 · Firefox 110 · Safari 16
Alignment scored
2026-08-23
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 @container — 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-23.

Rule Score Evidence
Typography 3 / 3 --bo-density-font-size on links; the 0.05em letter-spacing is intrinsic (same basis as combobox/form)
Colour 3 / 3 zero raw colour; hover/current use state + accent tokens
Spacing 3 / 3 the 14rem rail and the 3.25rem collapsed width both carry their reason, the collapsed one DERIVED from the icon slot plus padding; the visually-hidden 1px box is named as the idiom it is (94.6b)
Interaction 3 / 3 @container bo-shell collapse hides labels VISUALLY (clip-path), not display:none, so every link keeps its accessible name
Content 3 / 3 opener carries the wrong-choice clause: "Not for navigating within one screen" — every rail entry is a destination, so a record's own sections should scroll, and aria-current="page" there would claim a page change that never happened; points at object-page's anchor bar and tabs. Blind re-scored 2026-08-23 (Polish round 10); both demos verified to use aria-current on genuine destinations, matching the clause.
Fit 3 / 3 the shell rail; po-app renders it from ONE shared shell, so every screen carries it, and it composes inside .bo-offcanvas for the drawer pattern
↑↓ move Enter open Esc close