busy-office-ui
Menu

Dashboard

Widgets are named containers: a table inside a half-width widget auto-compacts while the same table full-width does not. Stat values use proportional figures (tabular-nums is reserved for columns); compact big values server-side (12.9K, $4.2M). One hero figure per view, maximum. Looking for a "Card"? .bo-widget is it — header/body/band composition, independent of dashboards; see "Widget parts" below. Not as a wrapper around every section of a page — a widget is a box that says "this content is a separate thing from the box beside it". Wrapping each section of one continuous form or record in its own bordered card draws lines where the reader needs continuity, and the page turns into a stack of containers with no hierarchy. A long record's own sections want form sections or object page's anchored headings; reach for widgets when the pieces are genuinely independent and could be reordered.

Open invoices 1,284 +12% increase, worse, vs July
Approved this week $412K +8% increase, better, vs last week
Avg. approval time 2.4d −0.6d decrease, better, vs Q2
Blocked 17 unchanged vs July

Hero figure + span-2 widget

Awaiting your approval 23 +6 increase, worse, since yesterday
Oldest waiting 9d

The hero tile spans two columns (.bo-widget--span-2) and collapses to one when the grid (a named container) drops below its two-column width. One hero per view, max.

Widget with an embedded table (container-driven compaction)

Pending approvals View all
Invoice #VendorCost centerAmount
INV-10234Acme Supply Co.CC-4021$4,208.00
INV-10237Umbrella LogisticsCC-2205$2,110.00
Notes

Resize the window: the table's secondary column hides when its widget gets narrow — container queries, not media queries.

Widget parts — header, body, band

.bo-widget is the framework's card primitive — filed under "Dashboard" because that's its most common use, but it composes into any card-shaped UI: a widget is __header / __body / __band. The band footer carries a status message (--msg) or actions — one durable strip. This is what record and approval cards are built from.

POActions band
Body content.
Message band
Body content.
Approved by J. Kim · 12 Aug 2026

Collapsible opt-in

data-collapse-trigger + initCollapsibleCards(): the trigger's aria-expanded and the panel's data-state are the two-channel contract (chevron rotation is decoration on top). CSS does the height transition — same grid-template-rows 0fr/1fr technique as the Motion module's .bo-motion-collapse. No data-state at all defaults to open, so this degrades to a plain always-open widget without the behavior wired up.

Shipping details

123 Warehouse Rd, Dock 4 · Carrier: Freightline · ETA 14 Aug

<div class="bo-widget">
  <header class="bo-widget__header">
    <!-- __title is a CLASS, not an element. On a real screen give it the
         heading that fits the page's outline — <h2> for a section widget,
         <h1> when the widget IS the record's identity (see the object-page
         pattern). The <span> below is only right when something else on the
         page already provides the heading. -->
    <h2 class="bo-widget__title">Shipping details</h2>
    <button class="bo-btn bo-btn--ghost bo-btn--icon" data-collapse-trigger
            aria-expanded="true" aria-controls="ship-body" aria-label="Toggle">
      <span class="bo-widget__toggle-icon">▾</span>
    </button>
  </header>
  <div class="bo-widget__collapse" id="ship-body" data-state="open">
    <div class="bo-widget__body">123 Warehouse Rd, Dock 4</div>
  </div>
</div>
<script type="module">
  import { initCollapsibleCards } from "@busy-office/ui/js";
  initCollapsibleCards(); // once, ever — event delegation
</script>

Markup

<div class="bo-widget">
  <div class="bo-stat">
    <span class="bo-stat__label">Open invoices</span>
    <span class="bo-stat__value">1,284</span>
    <span class="bo-stat__delta bo-stat__delta--bad">
      <span aria-hidden="true">▲</span> +12%
      <span aria-hidden="true">⚠</span>
      <span class="bo-visually-hidden">increase, worse,</span> vs July
    </span>
  </div>
</div>
<!-- Valence has two channels: the visible ⚠ on BAD deltas (arrow + sign only
     carry direction — a colorblind reader can't tell good ▲ from bad ▲) and
     the visually-hidden text for screen readers. Glyphs are aria-hidden so
     AT hears "+12%, increase, worse, vs July" without triangle noise. -->

Quick reference generated

ClassRole
.bo-badge block (root)
.bo-badge--type variant: type
.bo-stat block (root)
.bo-stat--hero variant: hero
.bo-stat__delta part: delta
.bo-stat__delta--bad variant: bad
.bo-stat__delta--good variant: good
.bo-stat__label part: label
.bo-stat__value part: value
.bo-widget block (root)
.bo-widget--span-2 variant: span-2
.bo-widget-grid block (root)
.bo-widget__band part: band
.bo-widget__band--msg variant: msg
.bo-widget__body part: body
.bo-widget__body--flush variant: flush
.bo-widget__collapse part: collapse
.bo-widget__header part: header
.bo-widget__title part: title
.bo-widget__toggle-icon part: toggle-icon

State attributes: data-collapse-triggerdata-statearia-expanded

API generated from CSS

Blocks bo-badgebo-statbo-widgetbo-widget-grid
Parts bo-stat__deltabo-stat__labelbo-stat__valuebo-widget__bandbo-widget__bodybo-widget__collapsebo-widget__headerbo-widget__titlebo-widget__toggle-icon
Variants bo-badge--typebo-stat--herobo-stat__delta--badbo-stat__delta--goodbo-widget--span-2bo-widget__band--msgbo-widget__body--flush
data-* hooks data-collapse-triggerdata-state="closed"
ARIA styled aria-expanded
Named containers bo-widgetbo-widget-grid
JS required Optional: initCollapsibleCards() — needed only for collapsible widgets. It flips a [data-collapse-trigger] button's aria-expanded and the data-state on the panel its aria-controls names; the CSS does the transition. A widget that never collapses needs no JS.
  • Delta valence needs two channels: the visible ⚠ on bad deltas AND visually-hidden text ("increase, worse,").
  • Stat values use proportional figures; compact big numbers server-side (12.9K, $4.2M). One hero per view.
  • Widgets are named containers (bo-widget); the grid is bo-widget-grid — span-2 collapses at the grid's 2-column threshold.
  • bo-widget__title is a CLASS, not an element — put it on the heading level the page's outline needs (h2 for a section widget, h1 when the widget carries the record's identity). Every demo here nests inside this page's own h1, so copying a span into a real screen is how a page ends up with none.
  • Collapsible is opt-in: data-collapse-trigger + initCollapsibleCards(). Without the behavior, a .bo-widget__collapse with no data-state just renders open.
Contrast — 29 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
danger-text danger-subtle 5.91 8.06
success-text success-subtle 4.79 10.17
danger-text bg-surface 6.47 8.89
danger-text bg-canvas 6.19 9.96
success-text bg-surface 5.02 12.02
success-text bg-canvas 4.8 13.46
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
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
border-control bg-muted 4.39 5.66
accent bg-muted 4.97 7.72
warning-strong bg-muted 4.56 6.69
warning-strong bg-surface 5.02 7.86
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 105 · Firefox 121 · 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.1.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 3rem is a display size deliberately above the scale top, left a literal because it has exactly one caller; a second is the trigger to promote it (94.13)
Colour 3 / 3 zero raw colour; delta valence is two-channel — a visible glyph plus visually-hidden text, per its own note
Spacing 3 / 3 every live dimension literal carries its reason in place: the 20rem is the fallback of the documented consumer-override hook --bo-widget-min, and the 41rem container threshold is derived in the comment directly above it (2 x 20rem min + 1rem gap); the only other literal is a font-size, scored under typography. 32px and 1rem occur in explanatory comments only. Corrected 2026-09-02 (Polish round 2): the previous wording counted 20rem among the comment-only numbers, where it is live in .bo-widget-grid's grid-template-columns, and never mentioned 41rem at all. Line number dropped 2026-09-04 (Polish round 1 on avatar): that reference named a line in dashboard.css which 249.8's 3-line @tagline header moved underneath it; the declaration is named instead, and does not move.
Interaction 3 / 3 the page heads its one JS-backed section "Collapsible (opt-in)" and draws the platform-versus-behavior line in both directions: data-collapse-trigger + initCollapsibleCards() flip aria-expanded and data-state, while CSS owns the 0fr/1fr height transition and a panel carrying no data-state renders open — so without the JS the widget degrades to a plain always-open card. The same sentence ships in the generated ApiTable notes, and check:claims drives both channels live. Blind re-scored 2026-09-02 (Polish round 2), correcting an `na`: the rubric admits `na` only where there is no interaction surface at all, and this component ships initCollapsibleCards (behaviors.json), whose hooks bo-widget__collapse / data-collapse-trigger / data-state are all on dashboard's own api.json surface.
Content 3 / 3 opener carries the wrong-choice clause: "Not as a wrapper around every section of a page" — a widget says the content inside is separate from the box beside it, so card-wrapping one continuous record draws lines where the reader needs continuity; points at form sections and object-page's anchored headings. Blind re-scored 2026-08-23 (Polish round 6), judged genuine and doing real work precisely because this page declares the widget to BE the Card two sentences earlier.
Fit 3 / 3 named containers whose width drives the density of what they hold; the overview surface the sidebar assigns it
↑↓ move Enter open Esc close