Menu
The W3C APG menu button pattern, on top of the native Popover API. The browser supplies the surface; this supplies the menu.
Example (a11y tier)
Open it, then use Up and Down, Home and End, or type a letter. Tab closes it and carries on; Escape and a click outside close it too, and those two are the browser's doing, not ours.
Import
import Menu from 'abaabil/menu' // structure only, no ARIAimport Menu from 'abaabil/menu/styled' // + CSSimport Menu from 'abaabil/menu/a11y' // + APG menu semantics and keyboardWhen a menu is the wrong widget
role="menu" means a list of actions, in the application-menu sense: the things you find under File and Edit. A button that reveals a few navigation links is not a menu. Marking it up as one makes a screen reader announce a widget the user then cannot operate as one, and it takes the links out of the page's normal reading order for no benefit.
For links, use popover and put an ordinary list inside it. That is lighter, needs no JavaScript, and is what the content actually is.
What the browser does, and what is left
The Popover API gives the panel its top layer, dismissal on an outside click and dismissal on Escape. None of that is reimplemented here, which is why the lower two tiers are server-renderable and why the a11y tier is as small as it is for a pattern this involved.
What the browser does not know is that this panel is a menu:
aria-haspopup, and anaria-expandedthat is kept in step with the panel's owntoggleevent rather than a second copy of the open state, because the browser can close the panel without telling the component.role="menu"on the panel,role="menuitem"on each entry.- A roving tabindex, so Tab leaves the menu instead of walking every item in it.
- Up and Down with wrapping, Home and End, skipping disabled items.
- Multi-character typeahead, with the buffer resetting after half a second.
- Focus moving to the first item on open, and back to the trigger on close.
Props
| Prop | Type | Default | Tier | Description |
|---|---|---|---|---|
id | string | — (required) | all | Panel id; also wires the trigger. Not generated, for the same reason as popover: that would mean useId, and a client boundary. |
trigger | ReactNode | — | all | Button content. |
items | Array<{ key?, label, href?, onSelect?, disabled? }> | — | all | An item with href renders an <a>, otherwise a <button>. |
triggerProps | object | — | all | Spread onto the trigger. |
label | string | — | a11y | Names the menu itself. Without it the menu is named by its trigger. |