Reference generated from the custom elements manifest — 6 properties, 1 slots, 1 events.
CODE — HTML / REACT
Guidance
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-controlspopulated 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
onMenuItemSelecton the<al-menu>itself, not on each item. The event bubbles and is composed, ande.detail.valuecarries the selected item'svalue. - Give the menu a
label. It becomes the list'saria-label, which is what distinguishes this list from every other list on the page. - Set
idon the menu and point the trigger'saria-controlsat it, so the control that opens the menu names what it opens. - Use
heightwhen 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"orrole="menuitem". Both were tried and removed: each item renders a real link or button, somenureported missing required children andmenuitemproduced an interactive role wrapping an interactive control. - Don't put arbitrary content in the default slot. The children are expected to be
al-menu-itemelements, and the roving tabindex logic walks them to find the focusable control inside each one. - Don't manage tab stops yourself. The component assigns
tabIndex0 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'slistrule does not descend through the role-less item hosts to find thelistiteminside 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-labelis valid onrole="list", which is what makeslabelthe 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 PARITY
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.
ACCESSIBILITY
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.
INSTALL
import '@southleft/al-web-components/components/menu';
import { ALMenu } from '@southleft/al-react';API — 6 PROPS
SLOTS
EVENTS
PUBLIC METHODS
SOURCE — components/menu/menu.ts · Molecules/Navigation/Menu