COMPONENTS / MOLECULES / al-menu← ALL COMPONENTS

Menu

<al-menu><ALMenu> REACTbetaOPEN IN FIGMA ↗

Reference generated from the custom elements manifest — 6 properties, 1 slots, 1 events.

PLAYGROUND1 CONTROL
PREVIEW / al-menu / ALTITUDE
Loading preview
Header Menu Item Menu Item Menu Item Menu Item Menu Item
FLAGS
CODE — HTML / REACT

Guidance

AUTHORED · CITED TO SOURCE

A vertical list of navigation destinations or item-level actions, with roving keyboard focus across its items. It is the list itself, not the popover around it: pair it with a trigger and a floating container when you need it to open and close.

WHEN TO USE

  • You are presenting several destinations or item actions in one list — a kebab menu on a row, a nav section, an account menu.
  • The list needs keyboard traversal: arrow keys move between items, Home and End jump to the ends, and only one item holds a tab stop at a time.
  • The items group under headings that expand and collapse. Group headers get their aria-controls populated with the ids of the items they own.

WHEN NOT TO USE

  • The list is a set of values the user picks from and the form then submits. That is a field, not a menu, and it needs the field treatment.USE SELECT →
  • You need the floating behaviour — anchored positioning, click-outside, open state. The menu does not position itself; put it inside the component that does.USE POPOVER →
  • You are rendering data rows rather than destinations. A menu carries navigation semantics that a plain content list should not claim.USE LIST →
  • There is exactly one action. A single-item menu is a button with an extra click in front of it.USE BUTTON →

DO

  • Bind onMenuItemSelect on the <al-menu> itself, not on each item. The event bubbles and is composed, and e.detail.value carries the selected item's value.
  • Give the menu a label. It becomes the list's aria-label, which is what distinguishes this list from every other list on the page.
  • Set id on the menu and point the trigger's aria-controls at it, so the control that opens the menu names what it opens.
  • Use height when the list can grow beyond the space you have — it constrains the menu and turns on vertical scrolling rather than letting the list push the layout.

DON’T

  • Don't add role="menu" or role="menuitem". Both were tried and removed: each item renders a real link or button, so menu reported missing required children and menuitem produced an interactive role wrapping an interactive control.
  • Don't put arbitrary content in the default slot. The children are expected to be al-menu-item elements, and the roving tabindex logic walks them to find the focusable control inside each one.
  • Don't manage tab stops yourself. The component assigns tabIndex 0 to the active item and -1 to the rest; a competing implementation will break arrow navigation.
  • Don't use a menu as a substitute for showing the two or three actions directly. Hiding a short list behind a click costs discoverability and buys nothing.

ACCESSIBILITY IN PRACTICE

  • The list is deliberately a plain list, not a menu. role="list" is written out explicitly even though <ul> already implies it, because axe's list rule does not descend through the role-less item hosts to find the listitem inside each shadow root — a bare <ul> across a shadow boundary reports a violation even when the flattened tree is correct.
  • Roving tabindex is the reason Tab does not walk every item. Before it, each item was its own tab stop, so Tab and the arrow keys did the same job and the list took as many presses to leave as it had entries.
  • aria-label is valid on role="list", which is what makes label the right way to name the menu.
  • Expanding a group header rewrites the visible item set and reassigns the keyboard indexes, so arrow navigation never lands on a hidden item.

CONTENT GUIDELINES

  • Write items as the action or the destination, in the same grammatical form throughout: all verbs, or all nouns, never a mix.
  • Keep items to a few words. A menu is scanned, and a wrapped item breaks the scan.
  • Name group headers for what the items have in common, not for the number of them.

WHERE THIS CAME FROM

  • libs/al-web-components/components/menu/menu.tsThe full recorded reasoning for `role="list"` over `role="menu"`, including the two axe rules that each alternative violated.
  • libs/al-web-components/components/menu/menu.tsRoving focus across items and the tab-stop problem it fixed.
  • libs/al-web-components/components/menu/menu.tsThe bubbled, composed selection event and the instruction to bind it on the menu rather than each item.
  • libs/al-web-components/components/menu-item/menu-item.tsEach item renders a real link or button — the fact that decides the whole role question above.

Each line above names a file and a literal string that must still appear in it.pnpm gate:guidance re-reads them from this built page, so guidance that has stopped being true fails a check rather than misleading you.

FIGMA AHEADAltitude Design SystemFIGMA SET · Menu1:1 ON EVERY PROPERTYFROM TRACKED PROJECTION

The Figma set changed since the last confirmed sync — the code is behind.

DECIDED BY
CONTRACT DIFF
FIGMA SIDE
OBSERVED
LAST CONFIRMED SYNC
2026-08-28
PUBLIC SURFACE
6 ATTR · 1 SLOT · 1 EVENT · 0 PART

The component’s public surface — attributes and their value sets, slots, events, CSS parts and custom properties, from the manifest — compared against the Figma set’s property definitions.

CHECKEVIDENCEDETAILRESULT
Default stateAXEmenu--DefaultPASS
Advanced statesAXE7 further storiesPASS
Screen readerMANUALNo screen-reader pass has been recorded for this component.NOT RECORDED
Keyboard navigationINTERACTION TESTSNo interaction tests and no manual keyboard pass.NOT RECORDED

MEASURED 2026-09-08 · 8 STORIES · AXE WCAG2A + WCAG2AA + WCAG21A + WCAG21AA + WCAG22AA

COLOR CONTRAST0 RULES FAILING

The CI gate disables color-contrast globally (libs/al-web-components/story-fixture/src/main.ts), so its result never reaches a test run. It is enabled for this measurement, and reported here whichever way it comes out.

NO CONTRAST VIOLATIONS ACROSS THIS COMPONENT’S 8 STORIES.

import '@southleft/al-web-components/components/menu';
import { ALMenu } from '@southleft/al-react';
PROPTYPEDEFAULT
variant'simple'Variant - **default** Display the menu items with a background and padding - **simple** Display the menu items without a background and padding
widthnumberWidth property - If set, the menu will be constrained to this width in px
heightnumberHeight property - If set, the menu will be constrained to this height in px and enable vertical scrolling
idstringId attribute - Unique id used for A11y - Associate the menu with the trigger that controls its open/close state
labelstringLabel attribute - Sets ariaLabel for A11y'Menu'
indentGroupItemsbooleanIndent group items attribute **true** - Applies padding to left align the text on group Menu Items with the text of the group Header **false** - Does not apply extra padding
SLOTDESCRIPTION
(default)The menu items in the menu (children must be `<al-menu-item>`).
EVENTDESCRIPTIONTYPE
onMenuItemSelectBubbled from `<al-menu-item>` (bubbles + composed). Bind the listener HERE on `<al-menu>`, not on each item. `e.detail.value` carries the selected item's `value` attribute. Example: <al-menu @onMenuItemSelect=${(e) => handleSelect(e.detail.value)}> <al-menu-item value="edit">Edit</al-menu-item> </al-menu>CustomEvent
METHODDESCRIPTION
syncHeadersWithItems()Dynamically associate Group Menu items with their Headers, and set indexes on items for keyboard navigation 1. Initalize the valid item count at 0 2. Iterate over all menu items 3. Initialize the items index to null 4. If the item is a Header: set its index, and increment the valid item count 5. If the item has an index greater than 0, set the margin block start to 12px 6. If the item is an Expandable Group Header: - Store it as the Group Header in the loop - Assign it a group id, if not previously set - Assign it a unique id, if not previously set 7. If the item is not a Header, a Group Header **has** been stored, and the item has does not already have a group id: - Assign the item the current group id - Apply the item a unique id - Add this unique id to the aria-controls attribute for the group's expand control 8. If required, apply padding to align the items's text with its Header 9. If the item's Group Header is expanded: assign its index and increment the valid item count 10. When the next item is not a header, remove the stored Group Header 11. If the item is not a Header or part of a group, assign its index and increment the valid item count
setIndentation()Dynamically set inline start padding on child menu items 1. Reset the padding to 0 2. If the Group Header has prefix content, add padding to account for prefix content width 3. If there is no prefix content, set the padding to the default width
setWidthHeight()Set the width and height 1. Add a custom property to adjust the width of the menu list 2. Add a custom property to adjust the height of the menu list and enable scroll
getNewValidItem()Find the previous or next valid item in the menu list that can receive focus
setFocusedItem()When a new item is focused: 1. Set the previously selected item's isFocused state to false 2. Store the newly focused item on the menu's state 3. Apply focus to the new item
setFocusAdjacentItem()Set the focused item to the previous or next menu item based on the 'isPrevious' flag
handleOnKeydown()Handle keydown event 1. On Down Arrow or Right Arrow key, move focus to the next item. Or, if focus is on the last valid item, move focus to the first valid item in the list. 2. On Up Arrow or Left Arrow key, move focus to the previous item. Or, if focus is on the first valid item, move focus to the last valid item in the list. 3. On Home key, move focus to the first valid item in the list. 4. On End key, move focus to the last valid item in the list.
handleOnMenuItemSelect()When a new item is selected: 1. Set the previously selected item's isSelected state to false 2. Store the newly selected item on the menu's state
handleOnMenuItemExpand()When a menu Group Header is Expanded: 1. Show/hide items that belong to the given Group Header, and reassign valid indexes for keyboard navigation

SOURCE — components/menu/menu.ts · Molecules/Navigation/Menu