Menubar
Overview
Menubar builds a persistent horizontal menu bar for application chrome: a row of triggers with roving focus that open dropdown menus on click or hover, with arbitrarily nested submenus and full keyboard navigation.
SMenubar is a data-driven aggregation component: pass an items array and it renders the full role="menubar" structure, triggers, dropdown content, and nested submenus. A top-level item can be either a trigger that opens a dropdown or a link that navigates (set href / to). Logic and accessibility semantics live in the Aria MenubarCompact; styles are injected through a scv() recipe.
SMenubardelegates all structural composition to the AriaMenubarCompact. For unstyled data-driven usage, import from@vean/aria/menubar; the dropdown layer reusesMenuOptionsCompactfrom@vean/aria/menu.
Features
- Data-driven composition — pass
items(MenuOptionData) to render the menubar; top-level items declare dropdowns viachildren, and child items supportlabel/icon/shortcut/disabled/separator/href/towith unlimited nesting. - Horizontal roving focus — the root keeps a single tab stop;
ArrowLeft/ArrowRightmove focus between triggers andloopwraps around at the ends; disabled triggers drop out of the focus order. - Full keyboard navigation —
Enter/Spacetoggle the menu andArrowDownopens it; arrow keys roam menu items,ArrowRight/ArrowLeftswitch between neighboring top-level menus;Escapecloses and restores focus. - Hover / pointer switching — with a menu open, hovering another trigger switches the open menu; hovering a link trigger dismisses the menu and focuses the link.
- Click / hover trigger modes —
triggersupportsclick(default) andhover:clickopens on click while hovering an open menubar still switches menus;hoveropens on hover (tune withdelayDuration/skipDelayDuration) and includes a pointer grace area so the pointer can move between the menubar and the content without accidental closes. - Overflow collapsing — with
collapsible, when the menubar content is wider than its container, trailing top-level items merge into a trailing “more” menu so the content always fits; customize viamoreLabel/moreIcon/moreProps/ themore-triggerslot. - Link top-level items — items with
href/torender as links (no dropdown), matching thenav-menupattern; link semantics fortarget/external/disabledare preserved. - Nested submenus — child items render arbitrarily deep submenus via
MenuSub, with arrow-key entry/exit and a pointer-grace debounce (100ms open delay). - Controlled / uncontrolled — with
modelValue, the open menu follows the prop and emitsupdate:modelValue; otherwisedefaultValueseeds the initial state. - Per-item and whole-bar disabling —
item.disabledmakes a single top-level trigger fully inert (aria-disabled+ out of tab order + blocked interaction); the compact-leveldisabledprop disables every trigger at once, including link triggers. - Hidden items —
item.hiddendrops a top-level trigger and its dropdown; hidden items are filtered before the overflow math, socollapsibleonly counts visible triggers. Hidden child items are dropped from their dropdown. - Bidirectional direction —
dirsupports LTR / RTL; arrow keys and submenu slide direction follow logical direction;portalPropscontrols whether content teleports tobody. - Six sizes —
size(xs…2xl) variants cover root and trigger spacing, padding, and font size. - Menu slot passthrough —
item-leading/item-trailing/trigger/item-link-iconslots forward to the menu layer for per-item customization; theuiprop overrides root and trigger slots. - Aria composition —
MenubarRoot/MenubarMenu/MenubarTrigger/MenubarContent/MenubarSubTrigger/MenubarSubContentandCompactare all exported from@vean/aria/menubar; menu primitives are reused from@vean/aria/menu.
Usage
Demos
API
Aria Layer
21Menubar<T>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | ClassValue | — | class of menubar root |
size | ThemeSize | — | Visual size of the component. |
ui | Partial<MenubarUi> | — | Per-slot class overrides for the component. |
indicatorPosition | AlignSide | — | Indicator position. |
disabled | boolean | — | Whether the component is disabled. |
placement | Placement | — | Placement. |
showArrow | boolean | — | Whether to show an arrow. |
triggerProps | MenubarTriggerProps | — | Properties forwarded to the trigger element. |
collapsible | boolean | — | When `true`, if the menubar content is wider than its container, the
trailing items collapse into a trailing "more" menu so the content always
fits inside the container. |
moreLabel | string | undefined | Label of the trailing "more" trigger when `collapsible`. |
moreIcon | IconValue | — | Icon of the trailing "more" trigger when `collapsible`. |
moreProps | MenubarTriggerProps | — | Properties forwarded to the trailing "more" trigger. |
portalProps | MenuPortalProps | — | Properties forwarded to the portal element. |
contentProps | MenubarContentProps | — | Properties forwarded to the content element. |
popupProps | MenuPopupProps | — | Properties forwarded to the popup element. |
arrowProps | MenuArrowProps | — | Properties forwarded to the arrow element. |
modelValue | T | — | The controlled value of the menu to open. Can be used as `v-model`. |
defaultValue | T | — | The value of the menu that should be open when initially rendered. |
dir | Direction | — | The reading direction of the menubar when applicable. |
loop | boolean | — | When `true`, keyboard navigation loops from last trigger to first and vice versa. |
trigger | MenubarTriggerType | 'click' | The trigger type of the menubar.
- `click`: The menu will be opened when the trigger is clicked (hovering
an open menubar still switches between menus).
- `hover`: The menu will be opened when the trigger is hovered. |
delayDuration | number | 150 | The duration from when the pointer enters the trigger until the menu gets
opened in hover mode. |
skipDelayDuration | number | 300 | How much time a user has to enter another trigger without incurring a
delay again. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
items | MenuOptionData<T>[] | — | Items rendered by the component. |
selectedValue | T | — | The selected value of the menu. |
itemProps | MenuItemProps | — | Properties forwarded to the item element. |
linkProps | LinkExtraProps | — | Properties forwarded to the link element. |
subProps | MenuSubProps | — | Properties forwarded to the sub element. |
groupProps | MenuGroupProps | — | Properties forwarded to the group element. |
groupLabelProps | MenuGroupLabelProps | — | Properties forwarded to the group label element. |
shortcutProps | MenuShortcutProps | — | Properties forwarded to the shortcut element. |
separatorProps | MenuSeparatorProps | — | Properties forwarded to the separator element. |
subTriggerProps | MenuSubTriggerProps | — | Properties forwarded to the sub trigger element. |
subContentProps | MenuSubContentProps | — | Properties forwarded to the sub content element. |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | T | Emitted when the model value changes. |
escapeKeyDown | KeyboardEvent | Event handler called when the escape key is down. Can be prevented. |
pointerDownOutside | PointerDownOutsideEvent | Event handler called when a `pointerdown` event happens outside of the `DismissableLayer`. Can be prevented. |
focusOutside | FocusOutsideEvent | Event handler called when the focus moves outside of the `DismissableLayer`. Can be prevented. |
interactOutside | PointerDownOutsideEvent | FocusOutsideEvent | Event handler called when an interaction happens outside the `DismissableLayer`. Specifically, when a `pointerdown`
event happens outside or focus moves outside of it. Can be prevented. |
openAutoFocus | Event | Event handler called when auto-focusing on open. Can be prevented. |
closeAutoFocus | Event | Event handler called when auto-focusing on close. Can be prevented. |
entryFocus | Event | Emitted when entry focus occurs. |
update:open | boolean | Event handler called when the open state of the dialog changes. |
select | MenuOptionData<T>, event: Event | Emitted when select occurs. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
item | { item: MenuOptionData<T>; isTrigger?: boolean; } | Custom content for the item slot. |
item-leading | { item: MenuOptionData<T>; } | Custom content for the item leading slot. |
item-trailing | { item: MenuOptionData<T>; } | Custom content for the item trailing slot. |
item-trigger-icon | { item: MenuOptionData<T>; } | Custom content for the item trigger icon slot. |
item-link-icon | { item: MenuOptionData<T>; } | Custom content for the item link icon slot. |
trigger | ((data: { item: MenuOptionData<T>; }) => any) | Custom content for the trigger slot.
Optional: `MenubarCompact` renders full default trigger content (icon,
label, link icon, trailing slot) when the consumer does not provide it. |
more-trigger | — | Custom content for the trailing "more" trigger when `collapsible`. |
MenubarArrow
MenubarCheckboxGroup
MenubarCheckboxItem
MenubarCompact<T>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | — | Whether the component is disabled. |
placement | Placement | — | Placement. |
showArrow | boolean | — | Whether to show an arrow. |
triggerProps | MenubarTriggerProps | — | Properties forwarded to the trigger element. |
collapsible | boolean | — | When `true`, if the menubar content is wider than its container, the
trailing items collapse into a trailing "more" menu so the content always
fits inside the container. |
moreLabel | string | undefined | Label of the trailing "more" trigger when `collapsible`. |
moreIcon | IconValue | — | Icon of the trailing "more" trigger when `collapsible`. |
moreProps | MenubarTriggerProps | — | Properties forwarded to the trailing "more" trigger. |
portalProps | MenuPortalProps | — | Properties forwarded to the portal element. |
contentProps | MenubarContentProps | — | Properties forwarded to the content element. |
popupProps | MenuPopupProps | — | Properties forwarded to the popup element. |
arrowProps | MenuArrowProps | — | Properties forwarded to the arrow element. |
modelValue | T | — | The controlled value of the menu to open. Can be used as `v-model`. |
defaultValue | T | — | The value of the menu that should be open when initially rendered. |
dir | Direction | — | The reading direction of the menubar when applicable. |
loop | boolean | — | When `true`, keyboard navigation loops from last trigger to first and vice versa. |
trigger | MenubarTriggerType | 'click' | The trigger type of the menubar.
- `click`: The menu will be opened when the trigger is clicked (hovering
an open menubar still switches between menus).
- `hover`: The menu will be opened when the trigger is hovered. |
delayDuration | number | 150 | The duration from when the pointer enters the trigger until the menu gets
opened in hover mode. |
skipDelayDuration | number | 300 | How much time a user has to enter another trigger without incurring a
delay again. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
items | MenuOptionData<T>[] | — | Items rendered by the component. |
selectedValue | T | — | The selected value of the menu. |
itemProps | MenuItemProps | — | Properties forwarded to the item element. |
linkProps | LinkExtraProps | — | Properties forwarded to the link element. |
subProps | MenuSubProps | — | Properties forwarded to the sub element. |
groupProps | MenuGroupProps | — | Properties forwarded to the group element. |
groupLabelProps | MenuGroupLabelProps | — | Properties forwarded to the group label element. |
shortcutProps | MenuShortcutProps | — | Properties forwarded to the shortcut element. |
separatorProps | MenuSeparatorProps | — | Properties forwarded to the separator element. |
subTriggerProps | MenuSubTriggerProps | — | Properties forwarded to the sub trigger element. |
subContentProps | MenuSubContentProps | — | Properties forwarded to the sub content element. |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | T | Emitted when the model value changes. |
escapeKeyDown | KeyboardEvent | Event handler called when the escape key is down. Can be prevented. |
pointerDownOutside | PointerDownOutsideEvent | Event handler called when a `pointerdown` event happens outside of the `DismissableLayer`. Can be prevented. |
focusOutside | FocusOutsideEvent | Event handler called when the focus moves outside of the `DismissableLayer`. Can be prevented. |
interactOutside | PointerDownOutsideEvent | FocusOutsideEvent | Event handler called when an interaction happens outside the `DismissableLayer`. Specifically, when a `pointerdown`
event happens outside or focus moves outside of it. Can be prevented. |
openAutoFocus | Event | Event handler called when auto-focusing on open. Can be prevented. |
closeAutoFocus | Event | Event handler called when auto-focusing on close. Can be prevented. |
entryFocus | Event | Emitted when entry focus occurs. |
update:open | boolean | Event handler called when the open state of the dialog changes. |
select | MenuOptionData<T>, event: Event | Emitted when select occurs. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
item | { item: MenuOptionData<T>; isTrigger?: boolean; } | Custom content for the item slot. |
item-leading | { item: MenuOptionData<T>; } | Custom content for the item leading slot. |
item-trailing | { item: MenuOptionData<T>; } | Custom content for the item trailing slot. |
item-trigger-icon | { item: MenuOptionData<T>; } | Custom content for the item trigger icon slot. |
item-link-icon | { item: MenuOptionData<T>; } | Custom content for the item link icon slot. |
trigger | ((data: { item: MenuOptionData<T>; }) => any) | Custom content for the trigger slot.
Optional: `MenubarCompact` renders full default trigger content (icon,
label, link icon, trailing slot) when the consumer does not provide it. |
more-trigger | — | Custom content for the trailing "more" trigger when `collapsible`. |
MenubarContent
Props
| Prop | Type | Default | Description |
|---|---|---|---|
popupProps | MenuPopupProps | — | Properties forwarded to the popup element. |
placement | Placement | undefined | The placement of the floating element.
If used, it will override the `side` and `align` props. |
side | Side | 'bottom' | The preferred side of the trigger to render against when open. Will be reversed when collisions occur and
avoidCollisions is enabled. |
sideOffset | number | 0 | The distance in pixels from the trigger. |
sideFlip | boolean | true | Flip to the opposite side when colliding with boundary. |
align | Align | 'center' | The preferred alignment against the trigger. May change when collisions occur. |
alignOffset | number | 0 | An offset in pixels from the `start` or `end` alignment options. |
alignFlip | boolean | true | Flip alignment when colliding with boundary.
May only occur when `prioritizePosition` is true. |
avoidCollisions | boolean | true | When `true`, overrides the side and align preferences to prevent collisions with boundary edges. |
collisionBoundary | Element | (Element | null)[] | null | [ ] | The element used as the collision boundary. By default this is the viewport, though you can provide additional
element(s) to be included in this check. |
collisionPadding | Padding | 0 | The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for
all sides), or a partial padding object, for example: { top: 20, left: 20 }. |
arrowPadding | number | 0 | The padding between the arrow and the edges of the content. If your content has border-radius, this will prevent it
from overflowing the corners. |
hideShiftedArrow | boolean | true | When `true`, hides the arrow when it cannot be centered
to the reference element. |
sticky | 'partial' | 'always' | 'partial' | The sticky behavior on the align axis. `partial` will keep the content in the boundary as long as the trigger is at
least partially in the boundary whilst "always" will keep the content in the boundary regardless. |
hideWhenDetached | boolean | false | Whether to hide the content when the trigger becomes fully occluded. |
positionStrategy | 'fixed' | 'absolute' | 'fixed' | The type of CSS position property to use. |
updatePositionStrategy | 'always' | 'optimized' | 'optimized' | Strategy to update the position of the floating element on every animation frame. |
disableUpdateOnLayoutShift | boolean | false | Whether to disable the update position for the content when the layout shifted. |
prioritizePosition | boolean | false | Force content to be position within the viewport. Might overlap the reference element, which may not be desired. |
reference | ReferenceElement | — | The custom element or virtual element that will be set as the reference to position the floating element. If
provided, it will replace the default anchor element. |
trapFocus | boolean | modal | Whether focus is trapped inside the popup while open (Tab cycles within the layer and body
scroll is locked for modal layers). |
disableHoverableContent | boolean | false | When `true`, hovering the popup closes instead of keeping it open (the grace area is disabled,
so leaving the trigger closes the layer immediately). |
onGracePointerExit | (() => void) | — | Callback invoked when the pointer finally leaves the grace area. Lets an upper layer (e.g.
Tooltip) run area-scoped close logic without registering its own second grace area. |
forceMount | boolean | — | Used to force mounting when more control is needed.
Useful when controlling animation with Vue animation libraries. |
loop | boolean | false | Whether keyboard navigation should loop around |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
escapeKeyDown | KeyboardEvent | Event handler called when the escape key is down. Can be prevented. |
pointerDownOutside | PointerDownOutsideEvent | Event handler called when a `pointerdown` event happens outside of the `DismissableLayer`. Can be prevented. |
focusOutside | FocusOutsideEvent | Event handler called when the focus moves outside of the `DismissableLayer`. Can be prevented. |
interactOutside | PointerDownOutsideEvent | FocusOutsideEvent | Event handler called when an interaction happens outside the `DismissableLayer`. Specifically, when a `pointerdown`
event happens outside or focus moves outside of it. Can be prevented. |
openAutoFocus | Event | Event handler called when auto-focusing on open. Can be prevented. |
closeAutoFocus | Event | Event handler called when auto-focusing on close. Can be prevented. |
entryFocus | Event | Emitted when entry focus occurs. |
MenubarGroup
MenubarGroupLabel
MenubarItem
MenubarItemIndicator
MenubarMenu
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | DefinedValue | — | A unique value that associates the trigger with the active root value when controlled. |
MenubarMenus<T>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | MenuOptionData<T>[] | — | Top-level items rendered as visible triggers. |
moreItems | MenuOptionData<T>[] | — | Items collapsed into the trailing "more" menu. |
rootProps | { [x: string]: unknown; } | — | Props forwarded to the menubar root. |
listeners | { [x: string]: unknown; } | — | Listeners forwarded to the menubar root. |
optionsProps | { [x: string]: unknown; } | — | Props forwarded to the menu options of each open menu. |
linkProps | LinkExtraProps | — | Props forwarded to link triggers. |
contentProps | MenubarContentProps | — | Props forwarded to the content of each menu. |
portalProps | MenuPortalProps | — | Props forwarded to the portal of each menu. |
getTriggerProps | (item: MenuOptionData<T>) => MenubarTriggerProps | — | Resolve the effective props of a top-level trigger item. |
moreTriggerProps | { [x: string]: unknown; } | — | Props forwarded to the trailing "more" trigger. |
moreLabel | string | — | Label of the trailing "more" trigger. |
moreIcon | IconValue | — | Icon of the trailing "more" trigger. |
MenubarPortal
MenubarRadioGroup
MenubarRadioItem
MenubarRoot<T>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | T | — | The controlled value of the menu to open. Can be used as `v-model`. |
defaultValue | T | — | The value of the menu that should be open when initially rendered. |
dir | Direction | — | The reading direction of the menubar when applicable. |
loop | boolean | — | When `true`, keyboard navigation loops from last trigger to first and vice versa. |
trigger | MenubarTriggerType | 'click' | The trigger type of the menubar.
- `click`: The menu will be opened when the trigger is clicked (hovering
an open menubar still switches between menus).
- `hover`: The menu will be opened when the trigger is hovered. |
delayDuration | number | 150 | The duration from when the pointer enters the trigger until the menu gets
opened in hover mode. |
skipDelayDuration | number | 300 | How much time a user has to enter another trigger without incurring a
delay again. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | T | Emitted when the model value changes. |
MenubarSeparator
MenubarSub
MenubarSubContent
Props
| Prop | Type | Default | Description |
|---|---|---|---|
loop | boolean | false | Whether keyboard navigation should loop around |
placement | Placement | undefined | The placement of the floating element.
If used, it will override the `side` and `align` props. |
popupProps | MenuPopupProps | — | Properties forwarded to the popup element. |
sideOffset | number | 0 | The distance in pixels from the trigger. |
arrowPadding | number | 0 | The padding between the arrow and the edges of the content. If your content has border-radius, this will prevent it
from overflowing the corners. |
updatePositionStrategy | 'always' | 'optimized' | 'optimized' | Strategy to update the position of the floating element on every animation frame. |
sideFlip | boolean | true | Flip to the opposite side when colliding with boundary. |
alignOffset | number | 0 | An offset in pixels from the `start` or `end` alignment options. |
alignFlip | boolean | true | Flip alignment when colliding with boundary.
May only occur when `prioritizePosition` is true. |
avoidCollisions | boolean | true | When `true`, overrides the side and align preferences to prevent collisions with boundary edges. |
collisionBoundary | Element | (Element | null)[] | null | [ ] | The element used as the collision boundary. By default this is the viewport, though you can provide additional
element(s) to be included in this check. |
collisionPadding | Padding | 0 | The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for
all sides), or a partial padding object, for example: { top: 20, left: 20 }. |
hideShiftedArrow | boolean | true | When `true`, hides the arrow when it cannot be centered
to the reference element. |
sticky | 'partial' | 'always' | 'partial' | The sticky behavior on the align axis. `partial` will keep the content in the boundary as long as the trigger is at
least partially in the boundary whilst "always" will keep the content in the boundary regardless. |
hideWhenDetached | boolean | false | Whether to hide the content when the trigger becomes fully occluded. |
positionStrategy | 'fixed' | 'absolute' | 'fixed' | The type of CSS position property to use. |
disableUpdateOnLayoutShift | boolean | false | Whether to disable the update position for the content when the layout shifted. |
prioritizePosition | boolean | false | Force content to be position within the viewport. Might overlap the reference element, which may not be desired. |
reference | ReferenceElement | — | The custom element or virtual element that will be set as the reference to position the floating element. If
provided, it will replace the default anchor element. |
trapFocus | boolean | modal | Whether focus is trapped inside the popup while open (Tab cycles within the layer and body
scroll is locked for modal layers). |
disableHoverableContent | boolean | false | When `true`, hovering the popup closes instead of keeping it open (the grace area is disabled,
so leaving the trigger closes the layer immediately). |
onGracePointerExit | (() => void) | — | Callback invoked when the pointer finally leaves the grace area. Lets an upper layer (e.g.
Tooltip) run area-scoped close logic without registering its own second grace area. |
forceMount | boolean | — | Used to force mounting when more control is needed.
Useful when controlling animation with Vue animation libraries. |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
escapeKeyDown | KeyboardEvent | Event handler called when the escape key is down. Can be prevented. |
pointerDownOutside | PointerDownOutsideEvent | Event handler called when a `pointerdown` event happens outside of the `DismissableLayer`. Can be prevented. |
focusOutside | FocusOutsideEvent | Event handler called when the focus moves outside of the `DismissableLayer`. Can be prevented. |
interactOutside | PointerDownOutsideEvent | FocusOutsideEvent | Event handler called when an interaction happens outside the `DismissableLayer`. Specifically, when a `pointerdown`
event happens outside or focus moves outside of it. Can be prevented. |
openAutoFocus | Event | Event handler called when auto-focusing on open. Can be prevented. |
closeAutoFocus | Event | Event handler called when auto-focusing on close. Can be prevented. |
entryFocus | Event | Emitted when entry focus occurs. |
MenubarSubTrigger
Props
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | — | When `true`, prevents the user from interacting with the item. |
textValue | string | — | Optional text used for typeahead purposes. By default the typeahead behavior will use the `.textContent` of the
item. <br> Use this when the content is complex, or you have non-textual content inside. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
MenubarTrigger
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type | ButtonType | 'button' | The type of the button element. Can be one of 'button', 'submit', or 'reset'. |
disabled | boolean | — | Whether the component is disabled. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Notes
Architecture & benchmark comparison
| Capability | VeanUI | Ant Design Menu | Element Plus Menu | Radix Menubar |
|---|---|---|---|---|
| Aria/styled split | ✅ @vean/aria/menubar + scv() | ❌ single package | ❌ single package | ✅ @radix-ui/react-menubar |
| Data-driven compact API | ✅ MenubarCompact + nested items | ✅ items | ✅ items | ❌ JSX composition |
| Top-level triggers | ✅ click / hover switch + arrows | ✅ click / hover | ✅ click / hover | ✅ click / hover + arrows |
| Horizontal roving | ✅ Roving Focus + loop | ✅ | ✅ | ✅ |
| Cross-menu switching | ✅ ArrowRight / ArrowLeft | ✅ | ✅ | ✅ |
| Nested submenus | ✅ any depth (reuses menu layer) | ✅ SubMenu | ✅ el-sub-menu | ✅ |
| Link top-level items | ✅ href / to + target / external / disabled | ✅ danger etc. | — | ✅ LinkItem |
| Disabled | ✅ per-item + compact-wide | ✅ disabled | ✅ disabled | ✅ disabled |
| Controlled mode | ✅ modelValue / defaultValue | ✅ openKeys / selectedKeys | ✅ default-active | ✅ value / onValueChange |
| Direction | ✅ LTR / RTL + logical arrow keys | ✅ RTL | ✅ RTL | ✅ RTL |
| Size variants | ✅ size xs…2xl | ✅ size | ✅ size | — |
| Menu item types | ✅ checkbox / radio / separator / shortcut (menu layer) | ✅ full | ✅ full | ✅ full |
Runtime considerations
- Open state and pointer — in the default mode (
trigger="click"), top-level triggers open their menu onpointerdown; clicking the same trigger again while open closes it via the dismissable layer (“click again to collapse”). Hovering another trigger switches the open menu. Withtrigger="hover"the menu opens on hover;delayDuration(default 150ms) controls the open delay andskipDelayDuration(default 300ms) the no-delay re-open window. - Link top-level items — items with
href/torender no dropdown; clicking or keyboard activation navigates directly. Hovering a link item while a menu is open collapses the menu and moves focus to the link. - Disabled semantics —
item.disabledtargets a single top-level item:aria-disabled,tabindex="-1", and blocked click/keyboard activation. The compact-leveldisableddisables every trigger (including link triggers). Disabled child items follow menu-layer semantics. - Portal and positioning — dropdown content teleports to
bodyby default (disable viaportalProps.disabled). If content appears in the wrong place, check fortransform/ animation-container ancestors; positioning relies ongetBoundingClientRectmeasurements. - Controlled / uncontrolled — with
modelValue, internal writes only emitupdate:modelValueand the open menu fully follows the prop; uncontrolled usage seeds the initial state withdefaultValue. - Focus restoration — keyboard paths (
Escape/ arrow switching) restore focus to the trigger precisely; pointer paths (click outside to close) do not move focus, matching the “pointer interaction does not hijack focus” convention. - RTL — with
dir="rtl", arrow-key semantics mirror (ArrowLeftbecomes “next”) and menu layout flips via logical properties. - Overflow collapsing — with
collapsible, trailing items merge into the “more” menu when the content overflows its container; measurement runs against the real rendered layout, so the container’s parent must provide a fixed/constrained width (e.g.max-w-*). If the menubar sits in a flex container that does not shrink, setmin-w-0or a width constraint on the parent.
FAQ
How do I make a top-level item navigate instead of opening a dropdown?
Set href or to on the item — link top-level items render as an <a> with no dropdown; use target / external to control how the link opens.
How do I precisely control which menu is open?
Bind modelValue with v-model. The top-level item value becomes the controlled value; activation emits update:modelValue, and external changes sync the open menu. Uncontrolled usage seeds the initial state with defaultValue.
How do I disable the whole menubar or single items?
Pass disabled to SMenubar to disable everything, or set disabled: true on a single top-level item. Disabled items render aria-disabled, drop out of tab order, and block click and keyboard activation.
What is the keyboard flow?
Tab enters the menubar; ArrowLeft / ArrowRight move between triggers; Enter / Space toggle a menu and ArrowDown opens it; inside a menu, arrow keys roam items, ArrowRight / ArrowLeft switch to neighboring top-level menus, and Escape closes. Disabled items are excluded from tab order.
Can I use checkboxes or radios inside submenus?
Yes — dropdown content reuses the menu layer: item-checked slots, MenuCheckboxItem / MenuRadioItem, plus separator / shortcut are all available; pass the matching fields on child items in compact usage.
Why does the menu collapse when hovering a link item?
Link top-level items have no dropdown; on pointer enter the menubar collapses the currently open menu and moves focus to the link — matching Radix Menubar’s link-item behavior and avoiding focus lingering on the trigger of a closed menu.
How do I make the menu open on hover instead of on click?
Pass trigger="hover" to SMenubar. Hovering a trigger opens its menu (delayDuration controls the open delay, skipDelayDuration the re-open window); the pointer can move between the menubar and the menu content without accidental closes. Keyboard operation (arrows / Enter / Space / Escape) is identical in both modes.
What if there are too many menu items to fit?
Pass collapsible to SMenubar. When the menubar content is wider than its container, trailing top-level items automatically merge into a trailing “more” menu (default label More, customizable via moreLabel / moreIcon / moreProps), so the content never exceeds the container width. The collapse state recomputes automatically when the container resizes.