Skip to content

Headless Core — Menu List

Powers Menu List internally. Unlike Dropdown and Context Menu, this factory has no open/close state of its own — it’s just the focusable item list, often used inside one of those two for the actual panel content.

import { createMenuList } from '@andersseen/headless-components/menu-list';
const menu = createMenuList({
ariaLabel: 'File actions',
items: [
{ id: 'edit', label: 'Edit' },
{ separator: true },
{ id: 'delete', label: 'Delete', intent: 'destructive' },
{ id: 'locked', label: 'Locked', disabled: true },
],
onSelect: id => console.log('Selected:', id),
});
const menuProps = menu.getMenuProps();
const itemProps = menu.getItemProps(item, index);
OptionTypeDefaultNotes
ariaLabelstring'Menu'
itemsMenuItemConfig[][]{ id?, label?, shortcut?, icon?, separator?, intent?, disabled? }.
onSelect(itemId: string) => void
rovingFocusbooleantrueSee Prop-getters below.

An item is only interactive (selectable/focusable) if separator is falsy and it has a non-empty id — items without an id render but are skipped by keyboard navigation and queries.getInteractiveItems().

items, ariaLabel, focusedIndex (-1 = none — an index into items, including separators, not into the interactive subset).

MemberSignatureNotes
actions.setItems(items)(items: MenuItemConfig[]) => voidResets focusedIndex to -1.
actions.focusItem(index)(index: number) => voidNo-ops for a separator or disabled item.
actions.selectItem(itemId)(itemId: string) => voidNo-ops for a disabled/non-interactive item; doesn’t move focus itself.
queries.getInteractiveItems()() => MenuInteractiveItem[]Filters out separators, items without id, and disabled items.
queries.getInteractiveItemIds()() => string[]
queries.getItemIndex(templateIndex)(templateIndex: number) => numberMaps a raw items index to its position among interactive items only — useful if your rendering layer needs a “1 of 3” style label.
GetterReturns
getMenuProps()role: 'menu', aria-label
getItemProps(item, index)role: 'menuitem', tabindex, aria-disabled, data-state, data-disabled
getSeparatorProps()role: 'separator'

With rovingFocus: true (the default), only the currently-focused item gets tabindex: 0 — the rest are -1, so Tab moves focus into and out of the whole menu in one step, and arrow keys move focusedIndex within it. With rovingFocus: false, every enabled item gets tabindex: 0 individually (the browser’s natural Tab order visits each one).

handleMenuKeyDown(event)ArrowDown/ArrowUp move the focus, Home/End jump to the first/last interactive item. handleItemKeyDown(event, item)Enter/Space select the item; everything else delegates to handleMenuKeyDown, so you only need to attach this one handler to each item element.

Focus next Select focused

Dropdown and Context Menu — the two disclosure factories this list is typically rendered inside.