Components / structure
Menubar
Radix is permitted here — Menubar is on the closed list of six.
Summary
A persistent horizontal bar of menus — the File / Edit / View strip.
| APG pattern | Menubar |
| Built on | Radix |
| Status | stable |
| Since | 0.1.0 |
<Menubar label="Editor" menus={…} onSelect={(item) => run(item.label)} />
Anatomy
A data-se="menubar" strip of triggers, each opening a floater that renders data-se="menu" so one set of rules dresses both.
| Part | Element | Required |
|---|---|---|
floater | <div> | yes |
item | <radixmenubar.subtrigger> | yes |
label | <span> | yes |
separator | <radixmenubar.separator> | yes |
shortcut | <span> | no |
trigger | <radixmenubar.trigger> | yes |
Properties
| Property | Values | Default | What it does |
|---|---|---|---|
menus | MenubarMenu[] | required | The top-level menus, in display order. Each holds its own items. |
label | string | none | Names the menubar. It persists on the page, so it is a region a reader meets repeatedly. |
onSelect | (item: MenubarItem) => void | none | Called with the activated item. The bar closes the open menu first. |
States
| State | Expressed as | |
|---|---|---|
| Focused | :focus-visible | |
| Hover | :hover | |
| Disabled | aria-disabled | |
| Highlighted | data-highlighted | |
| Open or closed | data-state |
Behavior
Left and Right move between triggers; Down, Up, Enter and Space open one. Up Arrow did nothing at all until the upstream port found it — Radix counts only Enter, Space and Down as keyboard opens, and the APG counts Up too.
Accessibility
One tab stop for the bar, with roving focus across the triggers.
Radix parks the tab stop on the menubar root rather than on an item, and gives the focused item tabindex="0" rather than -1. Both hold the requirement behind the APG's assertion, so the port asserts the invariant rather than the number.
Keyboard
| Key | Result |
|---|---|
Arrow Left / Arrow Right | Move between triggers |
Arrow Down / Arrow Up, Enter, Space | Open the focused menu |
Escape | Closes the open menu |
Tab | Leaves the bar entirely |
Roles, states and properties
| Attribute | Set by |
|---|---|
aria-label | set at render |
Focus
One tab stop for the bar, with roving focus across the triggers. Radix parks the stop on the menubar root and gives the focused item tabindex="0" rather than -1; both hold the requirement behind the APG's assertion. Arrow Up did nothing at all until the upstream port found it.
Screen reader
Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.
Contrast and target size
npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.
It takes the touch floor under (pointer: coarse) — the visual size is unchanged on a desktop pointer.
Tested
test/apg/batch-e.test.jsxtest/browser/keyboard.spec.mjs
Known gaps
- 10 upstream refs — APG's own page or markup, not the pattern
- 19 upstream refs — a navigation menubar; the rest is their page chrome
Appearance
| Token | Used for |
|---|---|
--se-accent | color |
--se-accent-soft | background |
--se-bg | background |
--se-border | background, border, border-block-end |
--se-border-width | border, border-block-end |
--se-control-h | min-block-size |
--se-control-h-sm | block-size |
--se-duration-fast | animation |
--se-ease-decelerate | animation |
--se-focus-color | outline |
--se-focus-offset | outline-offset |
--se-focus-width | outline, outline-offset |
--se-font-sans | font-family |
--se-icon-md | block-size, inline-size |
--se-icon-sm | block-size, inline-size |
--se-layer-floating | z-index |
--se-leading-ui | line-height |
--se-mix-soft | background |
--se-negative | background, color |
--se-radius-lg | border-radius |
--se-radius-md | border-radius |
--se-radius-sm | border-radius |
--se-shadow-floating | box-shadow |
--se-space-1 | gap, margin, padding, padding-block |
--se-space-2 | gap, inset-block-start, padding-inline |
--se-space-3 | padding-inline |
--se-surface | background |
--se-surface-highlight | background |
--se-surface-sunk | background |
--se-target-min | min-block-size |
--se-text | color |
--se-text-disabled | color |
--se-text-muted | color |
--se-text-sm | font-size |
Motion
Floaters rise over --se-duration-fast, the same as Menu.
Content
Trigger labels are the conventional ones where conventions exist — File, Edit, View. A menubar is a place readers arrive with expectations, and inventing names spends them.
Usage
Reach for this last. It is desktop-application furniture: it implies a density of commands most pages do not have, and it spends a permanent strip of screen saying so. On the web, a Menu attached to a button is usually the honest answer.
It earns its place when the app really is an editor — when there are more commands than fit anywhere else, and readers will return often enough to learn where things live.
Notes
Radix is permitted here — Menubar is on the closed list of six.