Vize

Menu

Accessible, unstyled Menu compound primitive (WAI-ARIA APG menu and menu button). Every menu surface — DropdownMenu, ContextMenu, and Menubar — shares this core: roving focus, typeahead, submenus with pointer grace, dismissal, focus return, positioning, portals, and presence.

Package @vizejs/ui/menu
Maturity stable
Own the source vize lib pull menu
Requires collection, context, controllable-state, dismissable-layer, focus-scope, id, inert-outside, pointer-grace, portal, positioner, presence, scroll-lock, typeahead
Aliases menu, action menu, submenu, menuitemcheckbox, menuitemradio
Covers WAI-ARIA menu pattern, Radix Menu, Reka UI Menu, React Aria Menu

Usage

import { Menu, MenuRoot, MenuArrow, MenuCheckboxItem, MenuContent, MenuGroup, MenuItem, MenuItemIndicator, MenuLabel, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuSub, MenuSubContent, MenuSubTrigger, MenuTrigger } from "@vizejs/ui/menu";

Or copy the source into your project with vize lib pull menu (see Source Distribution).

API

Source: src/families/menus/menu/menu-root.vue

Props

Prop Type Default Description
id string | null undefined Consumer-owned base id. null and undefined select a deterministic fallback.
open boolean undefined Controlled open state. undefined selects uncontrolled behavior.
defaultOpen boolean false Initial open state for uncontrolled use.
modal boolean true Make outside content inert and lock document scroll while open.
dir MenuDirection undefined Reading direction: flips submenu arrow keys and submenu placement. undefined inherits DirectionProvider/LocaleProvider, then "ltr".
loop boolean false Wrap arrow-key navigation from the last item to the first and back.
disabled boolean false Prevent opening and close the menu when it becomes disabled.

Events

Event Payload Description
update:open [value: boolean] Fired with the requested open value (supports v-model:open).
open-change [value: boolean, previous: boolean, nativeEvent: Event | null] Fired after any distinct open-state request with the previous value and native event.

Slots

Slot Slot props Description
default MenuSlotState Trigger and content. Receives the menu open state.

Source: src/families/menus/menu/menu-root.vue

Props

Prop Type Default Description
id string | null undefined Consumer-owned base id. null and undefined select a deterministic fallback.
open boolean undefined Controlled open state. undefined selects uncontrolled behavior.
defaultOpen boolean false Initial open state for uncontrolled use.
modal boolean true Make outside content inert and lock document scroll while open.
dir MenuDirection undefined Reading direction: flips submenu arrow keys and submenu placement. undefined inherits DirectionProvider/LocaleProvider, then "ltr".
loop boolean false Wrap arrow-key navigation from the last item to the first and back.
disabled boolean false Prevent opening and close the menu when it becomes disabled.

Events

Event Payload Description
update:open [value: boolean] Fired with the requested open value (supports v-model:open).
open-change [value: boolean, previous: boolean, nativeEvent: Event | null] Fired after any distinct open-state request with the previous value and native event.

Slots

Slot Slot props Description
default MenuSlotState Trigger and content. Receives the menu open state.

Source: src/families/menus/menu/menu-arrow.vue

Slots

Slot Slot props Description
default MenuArrowSlotState Decorative arrow contents. Receives coordinates for optional custom drawing.

Exposed

Member Type Description
x number | null Current arrow x coordinate when measured.
y number | null Current arrow y coordinate when measured.
element HTMLDivElement | null Rendered arrow element.

Source: src/families/menus/menu/menu-checkbox-item.vue

Props

Prop Type Default Description
modelValue MenuCheckedState undefined Controlled checked value (v-model). undefined selects uncontrolled behavior.
defaultValue MenuCheckedState false Initial checked value for uncontrolled use.
disabled boolean false Block activation while keeping the item focusable.
textValue string undefined Typeahead text when the rendered text is not representative.
closeOnSelect boolean true Close the whole menu tree after an unprevented selection.

Events

Event Payload Description
update:modelValue [value: boolean] Fired with the toggled checked value (supports v-model).
select [event: MenuSelectEvent] Fired when the item is activated, before toggling; call preventDefault() to keep the menu open.

Slots

Slot Slot props Description
default MenuCheckboxItemSlotState Item contents. Receives checked, highlight, and disabled state.

Exposed

Member Type Description
element HTMLDivElement | null Rendered role="menuitem" element.
id string Stable item id, also used as its collection key.
highlighted boolean Whether the item owns the highlight.
disabled boolean Whether activation is disabled.
focus () => void Move focus (and the highlight) to this item.
select (event?: Event | null) => boolean Activate the item as if the user selected it.
checked MenuCheckedState Current checked value.

Source: src/families/menus/menu/menu-content.vue

Props

Prop Type Default Description
forceMount boolean false Keep the content mounted while the menu is closed.
to string | HTMLElement "body" CSS selector or element the menu layer is moved into.
portalDisabled boolean false Render in place instead of teleporting.
defer boolean true Keep content in place until the target exists, avoiding SSR mismatch.
placement Placement "bottom-start" Preferred placement before collision handling.
strategy PositionerStrategy "fixed" CSS positioning mode published on the floating host.
offset number 4 Gap on the main axis between the reference and content.
collisionPadding number 4 Viewport padding the content should not cross.
arrowPadding number 0 Inset kept between the arrow and content edges.
flip boolean true Flip to the opposite side when the preferred side overflows more.
shift boolean true Shift the content back into the viewport after flip.
size boolean false Constrain the host and publish positioner available-size CSS variables.
safeArea boolean false Keep content clear of safe-area insets.
hide boolean true Hide when the reference no longer intersects the viewport.
updateOnScroll boolean true Recalculate while ancestors scroll.
updateOnResize boolean true Recalculate when the viewport resizes.
viewport Rect undefined Viewport used for flip, shift, and hide.
closeOnEscape boolean true Let Escape close the menu.
closeOnPointerDownOutside boolean true Let outside pointer-down close the menu.
closeOnFocusOutside boolean true Let outside focus movement close the menu.
ariaLabel string undefined Accessible name when the trigger does not label the menu.
ariaLabelledby string undefined Ids that label the menu; defaults to the trigger id when a trigger exists.

Events

Event Payload Description
open-auto-focus [event: MenuAutoFocusEvent] Fired before automatic entry focus; preventable.
close-auto-focus [event: MenuAutoFocusEvent] Fired before focus returns to the trigger; preventable.
escape-key-down [event: MenuEscapeKeyDownEvent] Fired before Escape closes the menu; preventable.
pointer-down-outside [event: MenuPointerDownOutsideEvent] Fired before an outside pointer-down closes the menu; preventable.
focus-outside [event: MenuFocusOutsideEvent] Fired before outside focus closes the menu; preventable.
interact-outside [event: MenuInteractOutsideEvent] Fired before any outside interaction closes the menu; preventable.
dismiss [event: MenuDismissEvent] Fired after an unprevented dismissal request.

Slots

Slot Slot props Description
default MenuContentSlotState Menu items. Receives open state and the resolved placement.

Exposed

Member Type Description
element HTMLDivElement | null Rendered role="menu" element.
open boolean Whether the content is open.
state MenuState Stable state token.
focusFirst () => HTMLElement | null Focus the first navigable item.
focusLast () => HTMLElement | null Focus the last navigable item.
focusContent () => void Focus the menu container and clear the highlight.

Source: src/families/menus/menu/menu-group.vue

Slots

Slot Slot props Description
default — Grouped items, usually led by a MenuLabel that names the group.

Source: src/families/menus/menu/menu-item.vue

Props

Prop Type Default Description
disabled boolean false Block activation. The item stays focusable and announced as disabled (WAI-ARIA APG).
textValue string undefined Typeahead text when the rendered text is not representative.
closeOnSelect boolean true Close the whole menu tree after an unprevented selection.

Events

Event Payload Description
select [event: MenuSelectEvent] Fired when the item is activated; call preventDefault() to keep the menu open.

Slots

Slot Slot props Description
default MenuItemSlotState Item contents. Receives highlight and disabled state.

Exposed

Member Type Description
element HTMLDivElement | null Rendered role="menuitem" element.
id string Stable item id, also used as its collection key.
highlighted boolean Whether the item owns the highlight.
disabled boolean Whether activation is disabled.
focus () => void Move focus (and the highlight) to this item.
select (event?: Event | null) => boolean Activate the item as if the user selected it.

Source: src/families/menus/menu/menu-item-indicator.vue

Props

Prop Type Default Description
forceMount boolean false Keep the indicator visible and its slot rendered while unchecked, for exit animations or glyphs that style the unchecked state themselves.

Slots

Slot Slot props Description
default MenuItemIndicatorSlotState Indicator glyph. Receives the owning item's checked-state token.

Source: src/families/menus/menu/menu-label.vue

Props

Prop Type Default Description
id string | null undefined Consumer-owned label id. null and undefined select a deterministic fallback.

Slots

Slot Slot props Description
default — Visible label text. Inside a MenuGroup it names that group.

Source: src/families/menus/menu/menu-radio-group.vue

Props

Prop Type Default Description
modelValue Value | null undefined Controlled value (v-model). undefined selects uncontrolled behavior; null clears.
defaultValue Value | null null Initial value for uncontrolled use.
disabled boolean false Block activation of every radio item in the group.
equals (left: Value, right: Value) => boolean Object.is Equality used to decide which item is checked (for example, compare object ids).

Events

Event Payload Description
update:modelValue [value: Value | null] Fired with the newly selected value (supports v-model).
value-change [value: Value | null, previous: Value | null, nativeEvent: Event | null] Fired after a distinct value is selected, with the previous value and native event.

Slots

Slot Slot props Description
default MenuRadioGroupSlotState<Value> Radio items. Receives the current value.

Exposed

Member Type Description
value Value | null Current group value.
setValue (value: Value | null) => boolean Request a group value; returns whether it changed.

Source: src/families/menus/menu/menu-radio-item.vue

Props

Prop Type Default Description
value (required) Value undefined Value committed to the enclosing MenuRadioGroup when this item is selected.
disabled boolean false Block activation while keeping the item focusable.
textValue string undefined Typeahead text when the rendered text is not representative.
closeOnSelect boolean true Close the whole menu tree after an unprevented selection.

Events

Event Payload Description
select [event: MenuSelectEvent] Fired when the item is activated, before the group value changes; preventable.

Slots

Slot Slot props Description
default MenuRadioItemSlotState Item contents. Receives checked, highlight, and disabled state.

Exposed

Member Type Description
element HTMLDivElement | null Rendered role="menuitem" element.
id string Stable item id, also used as its collection key.
highlighted boolean Whether the item owns the highlight.
disabled boolean Whether activation is disabled.
focus () => void Move focus (and the highlight) to this item.
select (event?: Event | null) => boolean Activate the item as if the user selected it.
checked boolean Whether this item holds the group value.

Source: src/families/menus/menu/menu-separator.vue

Source: src/families/menus/menu/menu-sub.vue

Props

Prop Type Default Description
id string | null undefined Consumer-owned submenu base id. null and undefined select a deterministic fallback.
open boolean undefined Controlled open state. undefined selects uncontrolled behavior.
defaultOpen boolean false Initial open state for uncontrolled use (only honored while the parent is open).
disabled boolean false Prevent the submenu from opening; its trigger stays focusable but inert.

Events

Event Payload Description
update:open [value: boolean] Fired with the requested submenu open value (supports v-model:open).
open-change [value: boolean, previous: boolean, nativeEvent: Event | null] Fired after any distinct open-state request with the previous value and native event.

Slots

Slot Slot props Description
default MenuSubSlotState MenuSubTrigger and MenuSubContent. Receives the submenu open state.

Exposed

Member Type Description
open boolean Whether the submenu is open.
state MenuState Stable state token.
triggerId string Id wired to the submenu trigger.
contentId string Id wired to the submenu content.
setOpen (value: boolean, event?: Event | null) => boolean Request the submenu open value.

Source: src/families/menus/menu/menu-sub-content.vue

Props

Prop Type Default Description
forceMount boolean false Keep the submenu mounted while closed.
to string | HTMLElement "body" CSS selector or element the submenu layer is moved into.
portalDisabled boolean false Render in place instead of teleporting.
defer boolean true Keep content in place until the target exists, avoiding SSR mismatch.
strategy PositionerStrategy "fixed" CSS positioning mode published on the floating host.
offset number 0 Gap between the submenu trigger and the submenu.
collisionPadding number 4 Viewport padding the submenu should not cross.
arrowPadding number 0 Inset kept between the arrow and content edges.
flip boolean true Flip to the opposite side when the reading-direction side overflows more.
shift boolean true Shift the submenu back into the viewport after flip.
size boolean false Constrain the host and publish positioner available-size CSS variables.
safeArea boolean false Keep content clear of safe-area insets.
hide boolean true Hide when the trigger no longer intersects the viewport.
updateOnScroll boolean true Recalculate while ancestors scroll.
updateOnResize boolean true Recalculate when the viewport resizes.
viewport Rect undefined Viewport used for flip, shift, and hide.
closeOnEscape boolean true Let Escape close this submenu and return focus to its trigger.
closeOnPointerDownOutside boolean true Let outside pointer-down close the submenu (and the tree when outside every menu).
closeOnFocusOutside boolean true Let outside focus movement close the submenu.
ariaLabel string undefined Accessible name overriding the submenu trigger label.

Events

Event Payload Description
open-auto-focus [event: MenuAutoFocusEvent] Fired before automatic entry focus; preventable.
close-auto-focus [event: MenuAutoFocusEvent] Fired before automatic focus restoration; preventable.
escape-key-down [event: MenuEscapeKeyDownEvent] Fired before Escape closes the submenu; preventable.
pointer-down-outside [event: MenuPointerDownOutsideEvent] Fired before an outside pointer-down closes the submenu; preventable.
focus-outside [event: MenuFocusOutsideEvent] Fired before outside focus closes the submenu; preventable.
interact-outside [event: MenuInteractOutsideEvent] Fired before any outside interaction closes the submenu; preventable.
dismiss [event: MenuDismissEvent] Fired after an unprevented dismissal request.

Slots

Slot Slot props Description
default MenuContentSlotState Submenu items. Receives open state and the resolved placement.

Source: src/families/menus/menu/menu-sub-trigger.vue

Props

Prop Type Default Description
disabled boolean false Block opening the submenu while keeping the trigger focusable.
textValue string undefined Typeahead text when the rendered text is not representative.

Slots

Slot Slot props Description
default MenuSubTriggerSlotState Trigger contents. Receives submenu open state plus highlight and disabled state.

Source: src/families/menus/menu/menu-trigger.vue

Props

Prop Type Default Description
disabled boolean false Remove the trigger from activation and sequential keyboard focus.
ariaLabel string undefined Accessible name when no visible label supplies one.

Events

Event Payload Description
click [nativeEvent: MouseEvent] Fired before a click toggles the menu. Call preventDefault() to keep state unchanged.
keydown [nativeEvent: KeyboardEvent] Fired before keyboard handling. Call preventDefault() to skip the built-in keys.

Slots

Slot Slot props Description
default MenuSlotState Trigger contents. Receives the menu open state.

Exposed

Member Type Description
element HTMLButtonElement | null Rendered native button.
focus (options?: FocusOptions) => void Move focus to the trigger.

Behavior

Normative state x input -> outcome table for the shared menu core (@vizejs/ui/menu), which also powers dropdown-menu, context-menu, and menubar. It follows the WAI-ARIA APG menu button and menu patterns. Every row is proven by the named test in menu.test.ts, menu-keyboard.test.ts, menu-pointer.test.ts, or menu-ssr.test.ts; compile-only guarantees live in menu.types.test-d.ts.

Components: menu-root.vue, menu-trigger.vue, menu-content.vue, menu-item.vue, menu-checkbox-item.vue, menu-radio-group.vue, menu-radio-item.vue, menu-item-indicator.vue, menu-group.vue, menu-label.vue, menu-separator.vue, menu-sub.vue, menu-sub-trigger.vue, menu-sub-content.vue, menu-arrow.vue.

# State Input Outcome Proven by
M1 closed render / pointer click on trigger native button with aria-haspopup="menu", aria-expanded, aria-controls while open; content is role="menu" labelled by the trigger and focused itself renders menu-button ARIA, deterministic ids, and pointer-opened content
M2 closed, trigger focus Enter / Space / ArrowDown / ArrowUp opens and focuses (and highlights) the first item, or the last item for ArrowUp trigger ${key} opens the menu and focuses ${expected} (four cases)
M3 open ArrowDown/Up, Home/End, PageDown/PageUp roving focus moves one item or to either end; no wrap unless loop arrow, Home/End, and Page keys move focus without wrapping by default
M4 open, loop ArrowUp on first / ArrowDown on last focus wraps loop wraps arrow navigation at both ends
M5 open printable characters locale-aware typeahead: prefix match, repeated letters cycle, textValue overrides rendered text typeahead matches prefixes, cycles repeated letters, and honors textValue
M6 open, pending query Space extends the query instead of selecting; Space selects once the query expires Space extends a pending typeahead query instead of selecting
M7 open (any depth) Tab the whole tree closes and focus returns to the trigger Tab closes the whole menu tree and returns focus to the trigger
M8 item highlighted Enter / click select fires with a preventable MenuSelectEvent; unprevented selection closes the tree and returns focus; preventDefault() keeps it open selecting an item emits a preventable select, closes the tree, and returns focus
M9 disabled item arrows / Enter / click stays focusable (APG), exposes aria-disabled and data-disabled, never selects disabled items stay focusable, announce aria-disabled, and never select
M10 checkbox item activation toggles aria-checked (mixed for indeterminate), data-state, indicator visibility, v-model; closeOnSelect=false keeps the menu open checkbox items toggle aria-checked, indicators, and honor closeOnSelect
M11 radio group <Value> activation commits the item's typed value, compares with equals, emits update:modelValue and value-change radio groups select generic values with custom equality
M12 controlled open trigger activation emits update:open and open-change; rendered state follows the prop only controlled open emits requests without opening until the parent accepts
M13 open outside pointer-down / Escape outside pointer closes without stealing focus; Escape closes and returns focus; closeOn* props disable either path outside pointer-down closes without stealing focus; Escape closes and returns focus
M14 open, modal render outside content is inert and scroll-locked (trigger stays live); non-modal menus leave the page interactive modal menus inert outside content; non-modal menus leave the page interactive
M15 disabled root activation / becoming disabled refuses to open and closes an open menu disabled roots refuse to open and close when disabled while open
M16 closed, forceMount render content stays mounted with hidden and data-state="closed"; no document controllers force-mounted closed content stays hidden and inactive
M17 any expose root openMenu/close/toggle/setOpen and ids; content focusFirst/focusLast/focusContent exposes root and content imperative APIs
M18 submenu trigger ArrowRight / Enter / Space opens the submenu and focuses its first item; ArrowLeft or Escape closes only the submenu and refocuses its trigger submenu keys: ArrowRight/Enter open, ArrowLeft/Escape close to the sub trigger
M19 dir="rtl" ArrowLeft / ArrowRight open/close keys swap and the submenu is placed on the left rtl flips submenu arrow keys and placement
M20 disabled MenuSub activation sub trigger stays focusable with aria-disabled and never opens disabled submenu triggers stay focusable but do not open
M21 open, mouse pointer move / leave on items hover highlights (focuses) items; leaving clears the highlight back to the menu hovering items moves the highlight; leaving an item clears it
M22 open hover disabled item / touch move disabled items never take the highlight; touch pointers never highlight hovering a disabled item clears the highlight; touch pointers never highlight
M23 open, mouse hover submenu trigger opens the submenu without moving focus into it; hovering submenu items highlights them hovering a submenu trigger opens it without moving focus into it
M24 submenu open pointer travels toward the submenu the pointer-grace safe triangle keeps the submenu open and suppresses sibling highlight; leaving it highlights the sibling and closes the submenu pointer grace keeps the submenu open along the safe triangle and closes it outside
M25 grace armed 300 ms elapse the triangle expires even if the pointer rests inside it pointer grace expires after its delay
M26 submenu open enter submenu / outside pointer-down entering clears the grace; pointer-down outside every level closes the tree entering the submenu clears the grace; clicking outside every menu closes the tree
M27 submenu open pointer-down in the parent menu closes only the submenu pointer-down inside the parent menu closes only the submenu
M28 submenu open select submenu item selection closes every level clicking a submenu item selects it and closes every level
M29 submenu closed pointer click on submenu trigger opens the submenu clicking a submenu trigger with a pointer opens it
M30 SSR, open two isolated requests byte-identical markup, content rendered in place, no inert/scroll-lock/highlight side effects renders deterministic open menu markup on the server
M31 SSR markup hydrate no mismatch diagnostics, root node reused, then portal and focus activate hydrates without mismatches and then activates focus

Extension hooks

Hook Values
parts trigger, content-host, content, sub-content-host, sub-content, item, sub-trigger, item-indicator, group, radio-group, label, separator, arrow
data data-vize-ui, data-state (open/closed, or checked/unchecked/indeterminate), data-highlighted, data-disabled, data-menu-kind, data-modal, data-side, data-align, data-placement, data-top-layer
CSS vars Positioner variables such as --vize-ui-positioner-available-width when size is enabled
composable useMenuRoot() builds custom root surfaces on the same tree/level contract