diff --git a/src/uix/eidos/components/menubar/index.ts b/src/uix/eidos/components/menubar/index.ts
new file mode 100644
index 000000000..a500021c6
--- /dev/null
+++ b/src/uix/eidos/components/menubar/index.ts
@@ -0,0 +1,106 @@
+// Menubar — eidos compound API.
+//
+// import { Menubar } from '$uix/eidos/components/menubar';
+//
+//
+//
+// File
+//
+//
+// New
+// Open…
+//
+//
+// Open recent
+//
+// project-1.svelte
+//
+//
+//
+//
+//
+// …
+//
+//
+// Soma owns the active-menu state, cross-menu arrow navigation, hover-
+// switch when active, focus return, typeahead, RTL. Eidos paints the
+// bar surface + trigger states; per-menu Content reuses the
+// dropdown-menu recipe (Item / Sub / Checkbox / Radio / Separator
+// chrome) since soma re-exports those DropdownMenu parts.
+//
+// Drops `Arrow` part — the menubar idiom anchors panels to the bar
+// edge; an arrow chrome doesn't fit. If you need one, render
+// `` inside Content (it'll work, just visually
+// awkward against the bar).
+import MenubarComponent from './menubar.svelte';
+import Menu from './menubar-menu.svelte';
+import Trigger from './menubar-trigger.svelte';
+import Content from './menubar-content.svelte';
+
+// Per-menu inner parts: re-use the eidos DropdownMenu wrappers so the
+// menu-indicator shared partial (commit a2709476) applies and we don't
+// duplicate Item/Sub/Checkbox/Radio recipes.
+import {
+ DropdownMenu as _DropdownMenu
+} from '$uix/eidos/components/dropdown-menu';
+import { Portal } from '$soma/components/internal';
+
+type MenubarNamespace = typeof MenubarComponent & {
+ Menu: typeof Menu;
+ Trigger: typeof Trigger;
+ Portal: typeof Portal;
+ Content: typeof Content;
+ Item: typeof _DropdownMenu.Item;
+ Group: typeof _DropdownMenu.Group;
+ GroupHeading: typeof _DropdownMenu.GroupHeading;
+ Separator: typeof _DropdownMenu.Separator;
+ Arrow: typeof _DropdownMenu.Arrow;
+ CheckboxItem: typeof _DropdownMenu.CheckboxItem;
+ CheckboxGroup: typeof _DropdownMenu.CheckboxGroup;
+ RadioGroup: typeof _DropdownMenu.RadioGroup;
+ RadioItem: typeof _DropdownMenu.RadioItem;
+ Sub: typeof _DropdownMenu.Sub;
+ SubTrigger: typeof _DropdownMenu.SubTrigger;
+ SubContent: typeof _DropdownMenu.SubContent;
+};
+
+const Menubar = MenubarComponent as MenubarNamespace;
+Menubar.Menu = Menu;
+Menubar.Trigger = Trigger;
+Menubar.Portal = Portal;
+Menubar.Content = Content;
+Menubar.Item = _DropdownMenu.Item;
+Menubar.Group = _DropdownMenu.Group;
+Menubar.GroupHeading = _DropdownMenu.GroupHeading;
+Menubar.Separator = _DropdownMenu.Separator;
+Menubar.Arrow = _DropdownMenu.Arrow;
+Menubar.CheckboxItem = _DropdownMenu.CheckboxItem;
+Menubar.CheckboxGroup = _DropdownMenu.CheckboxGroup;
+Menubar.RadioGroup = _DropdownMenu.RadioGroup;
+Menubar.RadioItem = _DropdownMenu.RadioItem;
+Menubar.Sub = _DropdownMenu.Sub;
+Menubar.SubTrigger = _DropdownMenu.SubTrigger;
+Menubar.SubContent = _DropdownMenu.SubContent;
+
+export { Menubar };
+export default Menubar;
+
+export type {
+ MenubarProps,
+ MenubarMenuProps as MenuProps,
+ MenubarTriggerProps as TriggerProps,
+ MenubarContentProps as ContentProps,
+ MenubarArrowProps as ArrowProps,
+ MenubarItemProps as ItemProps,
+ MenubarGroupProps as GroupProps,
+ MenubarGroupHeadingProps as GroupHeadingProps,
+ MenubarSeparatorProps as SeparatorProps,
+ MenubarCheckboxItemProps as CheckboxItemProps,
+ MenubarCheckboxGroupProps as CheckboxGroupProps,
+ MenubarRadioGroupProps as RadioGroupProps,
+ MenubarRadioItemProps as RadioItemProps,
+ MenubarSubProps as SubProps,
+ MenubarSubTriggerProps as SubTriggerProps,
+ MenubarSubContentProps as SubContentProps,
+ MenubarSize
+} from './types';
diff --git a/src/uix/eidos/components/menubar/menubar-content.svelte b/src/uix/eidos/components/menubar/menubar-content.svelte
new file mode 100644
index 000000000..e70fdf0d6
--- /dev/null
+++ b/src/uix/eidos/components/menubar/menubar-content.svelte
@@ -0,0 +1,30 @@
+
+
+
+ {#snippet children(snippetProps)}
+ {@render bodyContent?.(snippetProps)}
+ {/snippet}
+
diff --git a/src/uix/eidos/components/menubar/menubar-context.ts b/src/uix/eidos/components/menubar/menubar-context.ts
new file mode 100644
index 000000000..77fef045c
--- /dev/null
+++ b/src/uix/eidos/components/menubar/menubar-context.ts
@@ -0,0 +1,21 @@
+import { getContext, setContext } from 'svelte';
+import type { MenubarSize } from './types';
+
+/**
+ * Menubar size context — set by `` (Provider), read by
+ * `` so the size variant cascades to every per-menu
+ * panel without consumers needing to repeat the prop. Air did the same
+ * thing via `AIR_MENUBAR_CTX.getSize`; we just use Svelte's native
+ * `getContext`.
+ */
+const MENUBAR_SIZE_KEY = Symbol('eidos.menubar.size');
+
+export type MenubarSizeContext = { getSize: () => MenubarSize };
+
+export function setMenubarSizeContext(getSize: () => MenubarSize): void {
+ setContext(MENUBAR_SIZE_KEY, { getSize });
+}
+
+export function getMenubarSizeContext(): MenubarSizeContext | undefined {
+ return getContext(MENUBAR_SIZE_KEY);
+}
diff --git a/src/uix/eidos/components/menubar/menubar-menu.svelte b/src/uix/eidos/components/menubar/menubar-menu.svelte
new file mode 100644
index 000000000..cd30994ed
--- /dev/null
+++ b/src/uix/eidos/components/menubar/menubar-menu.svelte
@@ -0,0 +1,15 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/uix/eidos/components/menubar/menubar-trigger.svelte b/src/uix/eidos/components/menubar/menubar-trigger.svelte
new file mode 100644
index 000000000..3325c7213
--- /dev/null
+++ b/src/uix/eidos/components/menubar/menubar-trigger.svelte
@@ -0,0 +1,17 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/uix/eidos/components/menubar/menubar.css b/src/uix/eidos/components/menubar/menubar.css
new file mode 100644
index 000000000..8a9ebd682
--- /dev/null
+++ b/src/uix/eidos/components/menubar/menubar.css
@@ -0,0 +1,121 @@
+/*
+ * Menubar recipe — desktop File / Edit / View bar.
+ *
+ * Scope: provider bar surface + per-menu trigger states. Per-menu
+ * Content / Item / Sub-tree / Checkbox / Radio chrome is reused from
+ * dropdown-menu.css since soma's Menubar literally re-exports the
+ * DropdownMenu parts for those slots (parts emit the same
+ * `data-dropdown-menu-*` attrs and the shared menu-indicator partial
+ * picks them up automatically).
+ *
+ * What's menubar-specific:
+ *
+ * [data-menubar] → bar surface
+ * [data-menubar-trigger] → in-bar trigger ("File", "Edit", …)
+ * [data-menubar-content] → per-menu panel (same chrome as
+ * [data-dropdown-menu-content] via
+ * the dropdown-menu recipe; we only
+ * add size variants here)
+ *
+ * Sizes — sm | md | lg (xs and xl out of band for a desktop chrome bar).
+ */
+
+/* ── Provider — the bar itself ──────────────────────────────────────── */
+
+[data-menubar] {
+ --_menubar-height: var(--control-height-md);
+ --_menubar-padding: var(--space-1);
+ --_menubar-gap: var(--space-1);
+ --_menubar-radius: var(--radius-md);
+ --_menubar-trigger-px: var(--space-3);
+ --_menubar-font-size: var(--font-size-sm);
+
+ display: inline-flex;
+ align-items: center;
+ gap: var(--_menubar-gap);
+ padding: var(--_menubar-padding);
+ border: 1px solid var(--color-border-default);
+ border-radius: var(--_menubar-radius);
+ background: var(--color-surface-raised);
+}
+
+[data-menubar][data-size='sm'] {
+ --_menubar-height: var(--control-height-sm);
+ --_menubar-padding: calc(var(--space-1) * 0.5);
+ --_menubar-gap: calc(var(--space-1) * 0.5);
+ --_menubar-radius: var(--radius-sm);
+ --_menubar-trigger-px: var(--space-2);
+ --_menubar-font-size: var(--font-size-xs);
+}
+
+[data-menubar][data-size='lg'] {
+ --_menubar-height: var(--control-height-lg);
+ --_menubar-padding: var(--space-2);
+ --_menubar-gap: var(--space-2);
+ --_menubar-radius: var(--radius-lg);
+ --_menubar-trigger-px: var(--space-4);
+ --_menubar-font-size: var(--font-size-md);
+}
+
+/* ── Trigger — in-bar menu opener ───────────────────────────────────── */
+
+[data-menubar-trigger] {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-block-size: calc(var(--_menubar-height) - var(--_menubar-padding) * 2 - 2px);
+ padding-inline: var(--_menubar-trigger-px);
+ border: none;
+ border-radius: calc(var(--_menubar-radius) - var(--_menubar-padding));
+ background: transparent;
+ color: var(--color-content-primary);
+ font-family: var(--font-ui);
+ font-size: var(--_menubar-font-size);
+ font-weight: 500;
+ line-height: var(--leading-ui);
+ white-space: nowrap;
+ cursor: pointer;
+ outline: none;
+ transition:
+ background var(--duration-fast) var(--ease-default),
+ color var(--duration-fast) var(--ease-default);
+}
+
+[data-menubar-trigger]:hover:not([data-disabled]),
+[data-menubar-trigger][data-highlighted]:not([data-disabled]) {
+ background: var(--color-surface-overlay);
+}
+
+[data-menubar-trigger][data-state='open'],
+[data-menubar-trigger][data-state='open']:hover,
+[data-menubar-trigger][data-state='open'][data-highlighted] {
+ background: var(--color-primary-element);
+ color: var(--color-content-primary);
+}
+
+[data-menubar-trigger]:focus-visible {
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
+ outline-offset: 1px;
+ position: relative;
+ z-index: 1;
+}
+
+[data-menubar-trigger][data-disabled] {
+ color: var(--color-content-muted);
+ pointer-events: none;
+ cursor: default;
+}
+
+/* ── Content — per-menu panel ───────────────────────────────────────────
+ * Chrome (border, bg, shadow, padding, item rows, separator, sub-trigger
+ * chevron, group heading) comes from `dropdown-menu.css` via the
+ * `[data-dropdown-menu-*]` selectors that the Menu/Item/Sub parts emit
+ * (soma re-exports the DropdownMenu parts for the per-menu inner slots).
+ *
+ * We add only the menubar-specific marker selector here. The
+ * `[data-menubar-content]` selector matches the panel that wraps the
+ * Item rows — it's the same element that ALSO carries
+ * `[data-dropdown-menu-content]` because soma's MenubarContent
+ * specializes DropdownMenuContent. So the dropdown-menu panel rules
+ * already paint it; we don't repeat them.
+ */
diff --git a/src/uix/eidos/components/menubar/menubar.svelte b/src/uix/eidos/components/menubar/menubar.svelte
new file mode 100644
index 000000000..c58e4f6be
--- /dev/null
+++ b/src/uix/eidos/components/menubar/menubar.svelte
@@ -0,0 +1,48 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/uix/eidos/components/menubar/types.ts b/src/uix/eidos/components/menubar/types.ts
new file mode 100644
index 000000000..c1b1997a8
--- /dev/null
+++ b/src/uix/eidos/components/menubar/types.ts
@@ -0,0 +1,78 @@
+import type { ResponsiveProp, Size } from '$uix/eidos/lib/types';
+import type {
+ ProviderProps,
+ MenuProps,
+ TriggerProps,
+ ContentProps,
+ ArrowProps,
+ ItemProps,
+ GroupProps,
+ GroupHeadingProps,
+ SeparatorProps,
+ CheckboxItemProps,
+ CheckboxGroupProps,
+ RadioGroupProps,
+ RadioItemProps,
+ SubProps,
+ SubTriggerProps,
+ SubContentProps
+} from '$soma/components/menubar';
+
+/**
+ * Menubar visual size — three-step subset of the canonical `Size`
+ * scale, matching the air baseline. A menubar is a desktop affordance
+ * (File / Edit / View / …); the comfortable-density range is sm..lg,
+ * xs would crowd the triggers and xl is unnecessary for a chrome bar.
+ */
+export type MenubarSize = Extract;
+
+export type MenubarProps = ProviderProps & {
+ /**
+ * Visual size preset — `'sm' | 'md' | 'lg'`. Resolved reactively via
+ * `Eidos.resolve(size)`. Propagated to Content via context so the
+ * size variant cascades to per-menu panels.
+ *
+ * @default 'md'
+ */
+ size?: ResponsiveProp;
+};
+
+export type MenubarMenuProps = MenuProps;
+export type MenubarTriggerProps = TriggerProps;
+export type MenubarContentProps = ContentProps;
+
+// Re-exported from DropdownMenu — Menubar's per-menu inner parts
+// (Item, Group, Separator, Checkbox/RadioItem, Sub-tree) are literally
+// DropdownMenu parts. Eidos re-exports the dropdown-menu wrappers so
+// the menu-indicator shared partial (commit a2709476) applies
+// automatically.
+export type {
+ DropdownMenuArrowProps as MenubarArrowProps,
+ DropdownMenuItemProps as MenubarItemProps,
+ DropdownMenuGroupProps as MenubarGroupProps,
+ DropdownMenuGroupHeadingProps as MenubarGroupHeadingProps,
+ DropdownMenuSeparatorProps as MenubarSeparatorProps,
+ DropdownMenuCheckboxItemProps as MenubarCheckboxItemProps,
+ DropdownMenuCheckboxGroupProps as MenubarCheckboxGroupProps,
+ DropdownMenuRadioGroupProps as MenubarRadioGroupProps,
+ DropdownMenuRadioItemProps as MenubarRadioItemProps,
+ DropdownMenuSubProps as MenubarSubProps,
+ DropdownMenuSubTriggerProps as MenubarSubTriggerProps,
+ DropdownMenuSubContentProps as MenubarSubContentProps
+} from '$uix/eidos/components/dropdown-menu/types';
+
+// Re-export for callers that don't want to dive into soma types
+export type {
+ ArrowProps,
+ ItemProps,
+ GroupProps,
+ GroupHeadingProps,
+ SeparatorProps,
+ CheckboxItemProps,
+ CheckboxGroupProps,
+ RadioGroupProps,
+ RadioItemProps,
+ SubProps,
+ SubTriggerProps,
+ SubContentProps
+};
diff --git a/src/uix/eidos/index.css b/src/uix/eidos/index.css
index 03c8d8587..4321b2112 100644
--- a/src/uix/eidos/index.css
+++ b/src/uix/eidos/index.css
@@ -94,6 +94,7 @@
@import './components/alert-dialog/alert-dialog.css';
@import './components/dropdown-menu/dropdown-menu.css';
@import './components/context-menu/context-menu.css';
+@import './components/menubar/menubar.css';
/* Shared menu-item indicator partial — must load AFTER the menu recipes
so its `padding-inline-start` calc wins over the row rule's
`padding-inline` shorthand at equal specificity. */
diff --git a/web/routes/uix/components/menubar/+page.svelte b/web/routes/uix/components/menubar/+page.svelte
new file mode 100644
index 000000000..1a3205c66
--- /dev/null
+++ b/web/routes/uix/components/menubar/+page.svelte
@@ -0,0 +1,647 @@
+
+
+
+
+
Navigation · Menubar
+
Menubar
+
+ Desktop File / Edit / View bar. Exactly one menu open at a time;
+ hover a sibling trigger while a menu is open to switch without an
+ intermediate click. Soma owns active-menu state + cross-menu
+ ArrowLeft/Right + typeahead + focus return on Escape. Per-menu
+ Item / Sub / Checkbox / Radio chrome is shared with
+ DropdownMenu — same parts, same recipe, same
+ menu-indicator (checkbox/radio glyphs match the standalone
+ Checkbox / RadioGroup).
+
+ trace
+ {#if trace.length === 0}
+ click a trigger or arrow-nav between menus
+ {:else}
+ {#each trace.slice(0, 3) as entry (entry.at)}
+ {entry.event} · {entry.family}{entry.intent ? ' · ' + entry.intent : ''}
+ {fmtTime(entry.at)}
+ {/each}
+ {/if}
+
+ active {activeMenu || '—'}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if tab === 'live'}
+
+
Controls
+
+ Soma owns active-menu state + keyboard nav across menus. Eidos
+ adds size on the root and propagates it to per-menu Content via context.
+
+
+
+ soma Provider props
+
+
+
+
+
+
+
+
+ eidos visual size
+
+ · bar height + trigger padding + font-size + per-menu density
+
+
+
+
+
+
+
+ Bound state
+
+ · mirrors of CheckboxItem / CheckboxGroup / RadioGroup
+
+
+
+
+
+
+
+
+
+
+ soma
+ headless · active-menu state + cross-menu nav
+ svelte
+
+
{somaSnippet}
+
+
+
+
+ eidos
+ visual · bar surface + trigger states + size context
+ svelte
+
+
{eidosSnippet}
+
+
+ {/if}
+
+ {#if tab === 'api'}
+
+
API reference
+
+ Three menubar-specific parts (Provider, Menu, Trigger, Content)
+ plus DropdownMenu's full inner surface for Item / Sub /
+ Checkbox / Radio. The inner parts share recipes with
+ DropdownMenu — the
+ menu-indicator shared partial (checkbox/radio glyphs) applies
+ automatically.
+
+
+
Provider
+
+
+
Prop
Type
Default
Description
+
+
value bindable
string
''
Identifier of the currently open menu. Empty = none open.
+
onValueChange
(value: string) => void
—
Fires when the active menu changes.
+
dir
'ltr' | 'rtl'
'ltr'
Direction for cross-menu arrow nav.
+
loop
boolean
true
Wrap cross-menu arrow at edges.
+
size eidos
ResponsiveProp<'sm' | 'md' | 'lg'>
'md'
Visual size — bar height + trigger padding + per-menu density (via context).
+
+
+
+
+
Menu
+
+
+
Prop
Type
Default
Description
+
+
value
string
—
Unique identifier for this menu inside the menubar.
+
onOpenChange / onOpenChangeComplete
(open: boolean) => void
—
Per-menu open/close + animation-settle callbacks.
+
+
+
+
+
Trigger
+
+
+
Prop
Type
Default
Description
+
+
disabled
boolean
false
Disable opening this menu via this trigger.
+
…native button attrs
PrimitiveButtonAttributes
—
Renders a native <button>; all HTML button attrs flow through.
Stamped via context — no need to pass per Content.
+
+
+
+
+
Inner parts (shared with DropdownMenu)
+
+ Item, Group, GroupHeading,
+ Separator, Arrow, CheckboxItem,
+ CheckboxGroup, RadioGroup, RadioItem,
+ Sub, SubTrigger, SubContent —
+ identical API to DropdownMenu.
+ Eidos re-uses those wrappers so the recipe (and the
+ menu-indicator shared partial) is one source of truth.
+
+
+ {#each events as action (action.name)}
+ {@const sem = action.semantic}
+ {@const intentDecl = 'intent' in sem ? sem.intent : undefined}
+ {@const intentStr = typeof intentDecl === 'string' ? intentDecl : intentDecl ? `fromProp:${(intentDecl as { fromProp?: string }).fromProp ?? '?'}` : '—'}
+
+
{action.name}
+
{sem.family}
+
{sem.verb ?? '—'}
+
{sem.sequence ?? 'pre'}
+
{intentStr}
+
{action.target}
+
+ {/each}
+
+
+
+
+ {/if}
+
+ {#if tab === 'sema'}
+
+
+ sema
+ · events + perceptual signature
+
+
+ One event — commit-select — fires when an Item is
+ activated. Open/close of individual menus rides on
+ DropdownMenu's events via the re-exported parts.
+
+ Selectors at src/uix/eidos/components/menubar/menubar.css
+ (bar + trigger only). Per-menu Content / Item / Sub / Checkbox / Radio
+ chrome is reused from dropdown-menu.css + the shared
+ menu-indicator.css partial because soma's Menubar re-exports
+ the DropdownMenu parts for those slots.
+
+ Follows the APG menubar pattern.
+ Bar exposes role="menubar"; each per-menu Content is
+ role="menu". Trigger has aria-haspopup="menu",
+ aria-expanded and aria-controls.
+
+
+
Keyboard
+
+
+
Key
Action
+
+
ArrowRight / ArrowLeft on Trigger
Move focus to next/previous trigger (wraps if loop=true; RTL inverts).
+
Enter / Space / ArrowDown on Trigger
Open the menu, focus first item.
+
ArrowUp / ArrowDown on Item
Move within the menu.
+
ArrowLeft / ArrowRight on Item
Close current menu, focus + open prev/next menu.
+
Escape
Close the menu, return focus to its Trigger.
+
Home / End
Jump to first / last item.
+
Letter
Typeahead — focus next item starting with that letter.
+
+
+
+
+
Pointer
+
+
+
Gesture
Behavior
+
+
Click trigger
Opens its menu (or closes if already open).
+
Hover sibling trigger while a menu is open
Switches to the hovered menu without an intermediate click.