registry:ui · ARIA
menubar — File, Edit, View, ruled by the platform's gap
The application menu bar's top-level contract differs from stacked dropdowns, so it gets its OWN walker: Tab lands on the single roving stop; ←/→ move between triggers — and from an open bar the glide closes the current panel, opens the next, and focuses its first item; ↓/↑/Enter open; Home/End jump; Escape hands focus back to the trigger. The family composes: MenubarItem owns the ONE id, MenubarTrigger and MenubarPanel derive theirs (aria-controls always resolves), the panel registers its imperative handles at init — first registration wins — and the walkers are scoped to the nearest menu so nested dropdown families never leak.
$npx jixoai-ui add menubar
https://ui.jixoai.com/r/menubar.json
overview
Overview
A composed family — Item, Trigger, Panel, MenuItem — over one bar-owned walker, in-place top-layer panels, and the shared surface kernel.
The composition protocol is the family's spine: MenubarItem owns the ONE id (mount-stable by
contract), Trigger and Panel derive theirs
(${id}-trigger / ${id}-panel) so aria-controls always resolves regardless of
render order, and the panel registers its imperative show/hide handles at INIT under
the panel id — first registration wins, a conditionally removed panel leaves no ghost
handle. Both walkers are scoped with closest('[role=menu]'), so menubar panels AND
nested dropdown-menu families never leak into the bar walk. Separators stay plain <hr>.
The panel is a popover=manual role=menu surface: the family owns dismissal (a
document pointerdown outside-check plus panel Escape — popover=auto's light dismiss
raced the trigger click), CSS anchor positioning places it
(position-area: bottom span-right with a
flip-block fallback and anchors-visible scroll hiding; a viewport-center fallback for
engines without anchoring; a transparent ::backdrop — light dismiss must never dim).
Motion is the shared surface kernel, ONE instance PER PANEL — a single shared
animation slot would cancel the outgoing exit during a glide and ghost it at rest —
anchored to the item's slot span, the live axis. And the promotion is IN PLACE: the
panel renders inside the item's slot span and the top layer moves paint, not DOM, so
the bar's stamps inherit down — measured, stamping --jx-radius-effective: 12px on the bar
re-rendered the open panel at 12px. Body-appended portals would sever exactly that
inheritance; this family carries none.
The eight axes split by role (all measured in the axes table below): density is the
live two-channel ladder; radius is panel-only consumption; elevation splits bar-chrome
2xs from the panel's own level2 rung; theme is the split voice — live css chains
re-derive under .dark while the stylex atom snapshots stay frozen light. Kinship: navigation-menu (the declaration-scope sibling
whose panel promotes to the shared .jx-pop carrier — the menubar keeps its OWN panel
law instead) and dropdown-menu (the item walk
contract, duplicated deliberately so registry items stay independent).
Tab lands on File. ↓ opens and walks items; → from the open bar glides to Edit and focuses its first item; Escape returns to the trigger; click outside dismisses — the family owns the popover lifecycle. Selections echo in the footer.
types
Types
Compose top-level menus with linked triggers and panels, choosing the floating-surface variant at the root. Measured: auto resolves acrylic — blur(14px) saturate(1) brightness(2) on the surface body — while solid paints flat (backdrop-filter none).
usage
Usage
MenubarItem owns the stable id; Trigger and Panel derive their paired ids from it; the panel's children are MenuItems and plain separators.
<script lang="ts">
import {
Menubar,
MenubarItem,
MenubarTrigger,
MenubarPanel,
MenubarMenuItem,
} from '@ui/menubar/index';
</script>
<Menubar label="app">
<MenubarItem id="file">
<MenubarTrigger>File</MenubarTrigger>
<MenubarPanel>
<MenubarMenuItem href="/open">Open…</MenubarMenuItem>
<hr />
<MenubarMenuItem onselect={() => close()}>Close</MenubarMenuItem>
</MenubarPanel>
</MenubarItem>
</Menubar>theming
Density and tokens
The bar and its menu items consume the shared density rhythm (hit 28/40/48px, voice 11/13/15px, inset 8/12/16px across xs/default/lg — measured); the panel's anchor margin rides the family's own --jx-bar-gap (8px, measured — known limit: popover.css's uniform-margin flip does not read it, the anchor offset is fixed) and its surface fill rides the level2 supply.
| Token | Default | Source |
|---|---|---|
| --jx-bar-gap | 8px | component |
| --jx-hit | density scale | density |
| --jx-gap | density scale | density |
| --jx-inset | density scale | density |
| --jx-text | density scale | density |
| --jx-line | density scale | density |
| --jx-elevation-level2-surface | var(--surface-container-low) | structural |
api
API
The generated meta carries 14 props (13 named + the synthesized rest); the hand table’s density row folds into the shared universal section beneath (authored 3, served 2 — label and variant render here; the fold serves the eight axis rows). The universal fold serves the eight axis rows; the family-specific density MEASUREMENTS live in the axes table.
Menubar
| Property | Type | Default | Description |
|---|---|---|---|
| label | string | 'menu bar' | Accessible menubar landmark label. |
| variant | 'solid' | 'acrylic' | 'auto' | 'auto' · Own default, not ambient | Floating-surface paint for every panel in the bar. Defaults: literal slot — own ’auto’, ambient when an axis opens. On the surface body: auto resolves acrylic (blur(14px) measured), solid paints flat (backdrop-filter none measured). |
Universal props
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | the base scale — root font-size; parts size in em |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' · ambient scope | corner geometry (CSS corner-shape; §14 degrade table) |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | corner size; auto = the concentric broadcast (§3) |
| density | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | spacing/leading scale over the kernel channels (§4) |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' · ambient scope | the hue axis of the fixed oklch primary system (§5) |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' · ambient scope | light/dark profile; system = the JS-mutable global (§6) |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' · ambient scope | official M3 levels over the surface ladder (§7) |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' · ambient scope | intensity across the motion kernels (§8) |
MenubarItem / MenubarTrigger / MenubarPanel / MenubarMenuItem
| Property | Type | Default | Description |
|---|---|---|---|
| id | string | — | The ONE id, owned by MenubarItem and mount-stable; Trigger and Panel derive theirs from it. |
| href | string | — | Renders a navigating menu item when provided (an anchor role=menuitem). |
| onselect | (event: MouseEvent) => void | — | Runs an action, then the panel closes and focus restores to its trigger. |
axes
The eight axes on menubar
Density is the one live two-channel ladder (kernel geometry measured 28/40/48px hits; an explicit rung attr re-scopes the subtree in place). Radius is PANEL-ONLY consumption — the bar chrome never rounds; the panel's corner rides --jx-radius-consumed with corner-shape beside it. Elevation splits bar-chrome 2xs from the panel's own level2 rung. Theme is the SPLIT VOICE: poses and panel re-derive dark through live chains while the stylex atom snapshots stay frozen light. Size echoes on the bar root only; color and motion supply unread; no hover-open cascade exists to muddy the transitions.
| Property | Type | Default | Description |
|---|---|---|---|
| density | 'small' | 'medium' | 'large' | 'auto' | number (+ the five legacy spellings) | 'auto' | CONSUMED — the bar's geometry rides the density kernel channels, TWO channels deep. Measured across the scope ladder xs/default/lg: trigger min-block-size (--jx-hit) 28/40/48px, voice (--jx-text) 11/13/15px, line (--jx-line) 16/20/24px, inline padding (--jx-inset) 8/12/16px. Ambient (the bar's data-density attr renders null, measured) the channels cascade from the ancestor [data-density] scope; an EXPLICIT lane stamps the rung attr on the bar root and the same global scope selector re-scopes the subtree (measured data-density=lg → 48px/15px/16px in place). The panel mirrors bar.density through the family context. Number unit: coefficient. |
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | ROOT ECHO, NO GEOMETRY FOLLOWS — the §11 stamp moves the bar's own font-size (16px ambient, measured); nothing else scales: the parts' voices are the density kernel's px channels (--jx-text 13px at the default rung) and the family is em-free (grep receipt: the single rem value is the panel's 10rem min-width; zero readers of --jx-size-effective in ui/menubar/). Number unit: px. |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' | CONSUMED BY THE PANEL's corner-shape — menubar.css reads var(--jx-shape-effective, round) on .jx-menubar-panel (the §14 alias ladder; ambient round). The bar chrome itself is square: the bar atoms carry no border-radius at all. Number unit: none. |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | PANEL-ONLY CONSUMPTION — the bar root stamps --jx-radius-consumed (auto: the §3 concentric calc) and .jx-menubar-panel's border-radius reads it with the auto fallback verbatim. Measured chain, live: ambient 0px; stamping --jx-radius-effective: 12px on the bar re-rendered the open panel at 12px and back to 0px on removal — while the BAR itself never rounds. An explicit lane supplies the same way. Number unit: px. |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' | SUPPLY-ONLY — the hue carriers are stamped and unread (grep receipt: zero color-channel readers in ui/menubar/); the family paints the neutral surface and muted-ink voices. Number unit: hue degrees. |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' | THE SPLIT VOICE (measured per element) — explicit dark rides the .dark class bridge on the bar root. RE-DERIVING through live css chains: the trigger hover pose (--muted) measured oklch(0.2178 0 0) dark, the panel surface (--jx-elevation-level2-surface chain) dark acrylic, the panel ink (--popover-foreground) white — the in-place panel keeps the scope through its top-layer promotion (paint moved, not DOM). FROZEN as :root literal snapshots (stylex defineVars — the family stamps no tokenScope class): the bar card stayed oklch(1 0 0) while --card flipped to 0.3211, and the OPEN pose (--jx-muted) stayed 0.9551 light — a light open band on the dark bar while not hovered. system/auto = tree inheritance. No number lane. |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' | CONSUMED, SPLIT BY ROLE — the BAR is chrome: its card shadow is the typed --jx-shadow-2xs (1px 1px 0px measured), not the axis. The PANEL rides the family's own level2 (3dp, the menu rung — the MenubarDefaults own): the bar stamps --jx-elevation-effective: 3 plus the level2 surface/shadow supply vars (style attr measured), and the panel body carries the recipe (rgba(0,0,0,.3) 0 1px 2px + rgba(0,0,0,.15) 0 2px 6px 2px measured). An explicit lane re-supplies the panel. Number unit: dp. |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' | SUPPLY-ONLY AT THE AXIS — --jx-motion-effective has ZERO readers in ui/menubar/ and in surface-motion.ts (grep receipt); the open/close motion is the declarative WAAPI kernel's constant timeline (display/overlay 460ms allow-discrete; every visible property a formula of --jx-p; prefers-reduced-motion handled by media query, not the axis). The pose transitions that DO exist: trigger color/background 150ms ease-out, menu item 100ms (measured computed durations). No hover-open cascade: panels open by click or keys only (grep: no pointer handlers in the family). Number unit: coefficient. |
Receipts: the density ladder (28/40/48px · 11/13/15px · 8/12/16px), the explicit-rung re-scope (data-density=lg stamped in place), the radius chain (0 → 12px → 0 on the open panel), the theme split (--card flipped 0.3211 while the bar card stayed white; hover pose 0.2178 dark; open pose 0.9551 light), the level2 shadow recipe and the variant blur split were measured on this page's served DOM (probe, task 31); the supply-only and zero-reader rows carry grep receipts over ui/menubar/ and surface-motion.ts. The universal demo folds in below, with a query() seat: the density lane rides the md viewport key (48rem).
<script lang="ts">
import Menubar from '@ui/menubar/menubar.svelte';
import { query } from '@lib/universal-props-query.svelte';
</script>
<!-- below 48rem the base (small) applies; at 48rem+ the md case (large)
wins — 32px hits become 48px, the whole kernel ladder re-rungs -->
<Menubar label="responsive" density={query({ md: 'large' }, 'small')}>…</Menubar>Media keys are min-width: below 48rem the base (small — 32px hits, 12px voice) applies; at 48rem and wider the md case wins (large — 48px hits, 15px voice). The string lane takes both generics. Resize across 48rem.
density="large" — 48px hits, 15px voice
radius="medium" — open the panel: it rounds, the bar stays square
theme="dark" — the split voice: panel re-derives dark, atom chrome stays light
shape="bevel" — the panel's corner-shape, ambient round elsewhere
a11y
Accessibility
The application menu follows the menubar and menu keyboard patterns with a roving top-level tab stop and a one-hop glide; the walkers are scoped so nested menu families never leak.
Keyboard
| Key | Action |
|---|---|
| Tab | Enters the bar at the single roving stop — the bar trims to one tabbable trigger after mount (measured 0 / -1 / -1). |
| ← / → | Move between triggers; from an open bar the glide closes the current panel, opens the next, and focuses its first item (measured: focus lands inside the next panel — one hop per press). |
| ↓ / ↑ / Enter | Open the focused trigger's panel and focus its first item. |
| ↓ / ↑ / Home / End in a panel | Walk the open panel with wrapping — scoped to the nearest menu, so nested families never leak. |
| Escape | Close the panel and return focus to its trigger (measured). |
| Click outside | Dismisses — the family owns the popover lifecycle (popover=manual), so an outside-pointerdown check replaces auto light-dismiss. |
ARIA
| Attribute | Value | Description |
|---|---|---|
| role | menubar / menu / menuitem | Exposes the application menu hierarchy; triggers are buttons cast as menuitem. |
| aria-controls | derived panel id | Pairs each trigger with its panel through the id protocol — always resolves regardless of render order. |
| aria-haspopup | menu | Identifies triggers that open a menu. |
| aria-expanded | true / false | Mirrors the open-panel state on the trigger. |
| aria-label | Menubar landmark label | The label prop names the bar (default "menu bar"). |