Primitives Overlays
Menubar
The row of menus of a desktop application. Each menu is the Menu primitive in a popover; the menubar makes the row one widget with one tab stop, arrow keys between the items and between open menus, and hover switching. Tier 1.
- Tier
- 1 · Delegated micro-behavior
- Behavior JS
- 1457 B brotli · 1623 B gzip ·
packages/· methodprimitives/ artifacts/ size.json - Platform features
role="menubar",popovertarget,showPopover(),:popover-open,:has()- Shared listeners
- keydown, focusin, pointerover
- Per-instance listeners
- none
- Lazy state
- none
- Native base
role="menubar" of popovertarget menu buttons
On this page
Example
Open a menu with a click or ArrowDown.
Why this implementation exists
The platform already has most of a menubar: each menu is a popover opened by its item’s popovertarget, with light dismiss, Escape, focus return and the top layer, and the Menu primitive gives each menu its keys. What makes a row of menus a menubar in the WAI-ARIA pattern is the movement between them: one tab stop for the bar, the side arrows along it, and the side arrows inside an open menu moving to the neighbouring menu.
None of that needs memory. The open menu is the popover that matches :popover-open, the focusable item is the one with tabindex="0", and the menus are found through their items’ popovertarget. Three shared listeners (keydown, focusin, pointerover) cover every bar on the page.
The browser owns
- each menu: showing, light dismiss, Escape, focus return and the top layer (popovertarget)
- Enter and Space on an item open its menu (native button activation)
- the expanded state of each item, exposed from popovertarget
Svelte Lean owns
- one tab stop for the bar; the side arrows, Home and End between its items; RTL
- ArrowDown to open a menu; the side arrows inside a menu to move to the neighbouring one
- switching the open menu when the pointer moves to another item
- the menu behavior inside each menu (the menu contract)
- menubar.css: the bar, its items and the item whose menu is open
Usage
Install @svelte-lean/primitives for the behavior and @svelte-lean/styles for the stylesheet. With @svelte-lean/vite the
registration is injected for every static data-slean="menubar"; without it, import
the register module once.
import '@svelte-lean/primitives/menubar/register';import '@svelte-lean/styles/popover.css';
import '@svelte-lean/styles/menu.css';
import '@svelte-lean/styles/menubar.css';Render a role="menubar" list of role="none" items, each holding a role="menuitem" button with popovertarget and, right after it, its menu: a data-slean="menu" popover labelled by the item. Give the first item tabindex="0" and the others -1.
Listen to slean:select on the bar for the chosen item of any menu; checkbox and radio items keep the menu contract.
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <ul role="menubar" aria-label="…" data-slean="menubar"> | – | yes | No options. |
| item | <button role="menuitem" popovertarget="…" tabindex="0|-1"> | – | yes | Inside <li role="none">, before its menu; one has tabindex="0". |
| menu | <div popover role="menu" aria-labelledby="…" data-slean="menu"> | – | yes | The menu contract, labelled by its item. |
Runtime profile
The menubar registers keydown, focusin and pointerover handlers with the shared router; each menu registers the menu behavior's own. A thousand bars keep one listener per type (tests/menubar.test.ts).
Accessibility contract
role="menubar"named byaria-label; each item amenuitemwhose expanded state the platform exposes frompopovertarget; each menu labelled by its item.- One tab stop for the bar; the side arrows, Home and End move along it, mirrored under
dir="rtl". - ArrowDown, Enter or Space opens a menu and focus moves to its first item; Escape closes it and focus returns to the item.
- A menubar is for application commands. Site navigation is a
<nav>with links.
Keyboard
| Key | When | Result |
|---|---|---|
| ArrowRight/ArrowLeft | focus on the bar | Next or previous item, wrapping; swapped under dir="rtl" |
| Home/End | focus on the bar | First or last item |
| ArrowDown/Enter/Space | focus on an item | Opens its menu; focus moves to the first menu item |
| ArrowRight/ArrowLeft | focus in a menu | Closes it and opens the next or previous menu |
| Escape | focus in a menu | Closes it; focus returns to its item |
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
Popover API | Baseline 2024 (Chrome 114, Safari 17, Firefox 125) | The menus render inline; the bar still lists its items |
:has() | Baseline 2023 | The item whose menu is open is not drawn pressed |
Without JavaScript
Each item opens its menu natively through popovertarget; the menus' keys and the bar's arrows need the runtime, and Tab reaches only the first item.
Server rendering
Render the bar with one item at tabindex 0 and every menu closed.
Before hydration
Before the behaviors load, a click opens a menu (native) but the arrow keys do nothing. The first key after hydration works on the server's markup.
Styling
menubar.css lays the items out in a bar and draws the item whose menu is open as pressed, with :has() on the list item; the menus are drawn by popover.css and menu.css. The tokens it reads:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-space-1 | 0.25rem | padding |
--slean-border | var(--slean-neutral-6) | border |
--slean-radius-md | 0.625rem | border-radius |
--slean-surface | oklch(100% 0 0) | background |
--slean-control-height-sm | 2rem | min-block-size |
--slean-space-3 | 0.75rem | padding-inline |
--slean-radius-sm | 0.375rem | border-radius |
--slean-fg | var(--slean-neutral-12) | color |
--slean-text-sm | 0.875rem | font-size |
--slean-font-weight-medium | 500 | font-weight |
--slean-muted | var(--slean-neutral-3) | background |
--slean-focus-ring | var(--slean-focus-ring-width) solid var(--slean-focus-ring-color) | outline |
--slean-focus-ring-width | 2px | outline-offset |
--slean-accent-soft | oklch(95% 0.03 258) | background |
--slean-accent-soft-fg | oklch(42% 0.17 258) | color |
--slean-control-height-md | 2.25rem | min-block-size |
State selectors the stylesheet targets, all from the platform or ARIA: :focus-visible, :hover, :popover-open, [aria-disabled="true"], [role="menu"], [role="menuitem"].
Compatibility notes
The Popover API is Baseline 2024 and :has() Baseline 2023. Submenus and mnemonics are not part of the primitive.
Testing
packages/primitives/tests/menubar.test.tsVitest: the bar items, arrows and RTL, opening, moving between menus, hover, 1000 barsapps/playground/tests/primitives/layout.spec.tsPlaywright: the splitter and the context menu in Chrome, with axepackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)
Source
packages/primitives/src/menubar/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/menubar/contract.tstyped constants: name, tier, base, parts, options, eventspackages/primitives/src/menubar/behavior.tsthe behavior definitionpackages/primitives/src/menubar/register.tsthe registration modulepackages/styles/css/menubar.cssthe optional stylesheet