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 # | Vendor | Actions |
|---|---|---|
| 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
| Class | Role |
|---|---|
| .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-multiselectto the menu and use real<input type="checkbox">items (wrapped in a<label>, not a button). SameinitDropdowns()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)
| Text | on | Light | Dark |
|---|---|---|---|
| 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 |