busy-office-ui
Menu

Dropdown

The menu is a native [popover]: it renders in the browser top layer, so it is never clipped by scrolling table containers and never loses a z-index fight with sticky headers or frozen columns. Esc and light dismiss are native (and Esc closes the innermost layer first inside a dialog). initDropdowns() adds invoker anchoring and close-on-select. Not for two or three options that should all be visible — hiding a short, mutually exclusive choice behind a click costs a click and hides the alternatives; that is Segmented control. Not for picking one value out of a long list — a menu is scanned, and past a screenful scanning is the wrong task; when the reader needs to type to narrow it down, that is Combobox. The multi-select menu below is the other half of that boundary: several values, from a list short enough to read.


Inside a data table (the point of the top layer)

Invoice #VendorActions
INV-10234 Acme Supply Co.

The row menu escapes the scrolling container — with an absolutely-positioned menu this would be clipped.

Multi-select

Real checkboxes, not a custom widget — data-multiselect on the menu keeps it open across selections and updates the trigger's label ("Cost center" → "Cost center (2)") from data-multiselect-label. Tab + Space, no extra keyboard wiring needed. Every other menu on the page still closes on select.

This is the framework's answer to "choose which of these apply" — including the case a desktop ERP solves with two side-by-side lists (available on the left, selected on the right, arrows between). There is no dual-list component and there will not be one while this exists: it would be a second way to do the same job, it invents an interaction model that has to be taught, and it does not fit — three columns inside the 342px a docs page has at 390px leaves about 114px each, when a single task label measured 128px at spacious density. A checkbox menu is one column, works at any width, and gets its keyboard and screen-reader behaviour from the platform (roadmap 132.5).

<button class="bo-btn bo-btn--secondary" popovertarget="cc-menu" data-multiselect-label="Cost center">Cost center</button>
<div class="bo-dropdown__menu" id="cc-menu" popover data-multiselect>
  <label class="bo-dropdown__item"><input type="checkbox" class="bo-checkbox" name="cc" value="CC-1180" /> CC-1180</label>
  <label class="bo-dropdown__item"><input type="checkbox" class="bo-checkbox" name="cc" value="CC-2205" /> CC-2205</label>
  <label class="bo-dropdown__item"><input type="checkbox" class="bo-checkbox" name="cc" value="CC-4021" /> CC-4021</label>
</div>
<script type="module">
  import { initDropdowns } from "@busy-office/ui/js";
  initDropdowns(); // same call — multi-select is data-multiselect on the menu, no separate init
</script>

Items with an icon and a trailing shortcut

No new class: .bo-dropdown__item is already display: flex, so a leading icon (aria-hidden, same rule as an icon beside any label) and a trailing keyboard-key compose for free — margin-inline-start: auto on the last child pushes it to the far edge, plain flexbox, no framework CSS needed for the push itself.

Markup

<button class="bo-btn bo-btn--secondary" popovertarget="row-actions">Actions ▾</button>
<div class="bo-dropdown__menu" id="row-actions" popover>
  <button class="bo-dropdown__item" type="button">Duplicate</button>
  <button class="bo-dropdown__item" type="button">Export PDF</button>
  <hr class="bo-dropdown__separator" />
  <button class="bo-dropdown__item bo-dropdown__item--danger" type="button">Void</button>
</div>
<script type="module">
  import { initDropdowns } from "@busy-office/ui/js";
  initDropdowns(); // anchors the menu to its invoker + close-on-select
</script>

Quick reference generated

ClassRole
.bo-dropdown__item part: item
.bo-dropdown__item--danger variant: danger
.bo-dropdown__menu part: menu
.bo-dropdown__menu--end variant: end
.bo-dropdown__separator part: separator

API generated from CSS

Blocks bo-dropdown
Parts bo-dropdown__itembo-dropdown__menubo-dropdown__separator
Variants bo-dropdown__item--dangerbo-dropdown__menu--end
JS required Recommended: initDropdowns() — anchors the top-layer popover to its invoker and closes on select. Without it the menu opens centered.
  • The menu is a native [popover]: never clipped by scroll containers, never z-fought by sticky headers; Esc and light dismiss are native.
  • Multi-select: add data-multiselect to the menu and use real <input type="checkbox"> items (wrapped in a <label>, not a button). Same initDropdowns() call — no separate init function.
  • Open/close motion needs Firefox 129 / Safari 17.5, the degrades tier of the browser floor — below it the menu still opens and closes, just without the fade.
Contrast — 14 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
bg-surface text-primary 17.74 16.15
danger-text danger-subtle 5.91 8.06
danger-text bg-surface 6.47 8.89
danger-text bg-canvas 6.19 9.96
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
accent bg-selected 5.25 6.66
text-primary bg-muted 16.12 13.75
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 no raw font-size
Colour 3 / 3 zero raw colour
Spacing 3 / 3 its one dimension literal carries its reason
Interaction 3 / 3 a native [popover] in the top layer — never clipped by a scrolling table container, never in a z-index fight with a sticky header; Esc and light dismiss come from the platform, initDropdowns only positions
Content 3 / 3 the opener carries the required wrong-choice clause and links the alternative (94.10)
Fit 3 / 3 row and toolbar menus; the page names the multi-select variant and the context-menu composition
↑↓ move Enter open Esc close