Use a menu button to offer a short list of actions on one thing: a work order, a van, a table view. For choosing a value in a form, use a select or combobox; for navigation, use links (top nav, side nav) — never role="menu" for site navigation.
Keep menus to 3–9 items; group with separators and group labels. Put destructive items last, in red, and confirm them.
Item labels are verb-first and sentence case ("Print job sheet"). The trigger names the object or "Actions"; icon-only triggers name the item ("More actions for van KX-219").
Checkable items (menuitemcheckbox, menuitemradio) keep the menu open; plain actions close it.
One submenu level at most; deeper trees belong in a dialog or page.
Anatomy
Trigger — .btn or .icon-btn with aria-haspopup="menu", aria-expanded, aria-controls, commandfor/command="toggle-popover"
Surface — div.menu[popover][role=menu], anchored with CSS anchor positioning
Shortcut or submenu chevron — kbd.menu__shortcut / .menu__chevron
Group + label, separator — .menu__group[role=group], .menu__group-label, hr.menu__separator
Examples
Live demos open real popovers (top layer, light dismiss, Esc — no JavaScript needed to open or close). js/menu.js adds roving focus, typeahead, submenu arrows and checkable items. Frozen previews are inert copies of the open surface. States marked hover, focus-visible and active are frozen with .is-hover, .is-focus-visible, .is-active. Those classes share their rule with the real pseudo-class, so what you see is exactly what users get. Never use them in product code.
Menu button
Live: click, or focus and press ↓. Esc closes and returns focus.
Frozen preview of the open surface.
Shortcuts are hints; the real shortcuts work without opening the menu.
Show columns
Sort by
Show columns
Sort by
→ opens the submenu, ← closes it (mirrored in RTL).
Disabled items stay focusable so people can discover them; the reason goes next to the trigger: "Log at least one task before closing".
Fired on the item: detail { item, value, checked }
init(root)
JS (js/menu.js)
Enhances every [aria-haspopup=menu][aria-controls]
Do and don't
Do group related actions and put destructive ones last.
Don't use a menu for site navigation — use links in a nav.
Accessibility
Keyboard interaction
Key
Behavior
Enter / Space / ↓ (trigger)
Opens and focuses the first item
↑ (trigger)
Opens and focuses the last item
↓ / ↑
Next / previous item (wraps)
Home / End
First / last item
Letter keys
Typeahead to the next item starting with those letters
Enter / Space (item)
Runs the action and closes; toggles checkable items
→ / ←
Open / close a submenu (mirrored in RTL)
Esc
Closes the current level; focus returns to its trigger
Tab
Closes the whole menu and moves on
Pattern: WAI-ARIA APG Menu Button. The trigger's aria-expanded is kept in sync on every open and close.
Disabled items use aria-disabled="true", stay reachable with arrows, and do nothing when activated.
Checked state: a check icon plus aria-checked — never color alone.
Placement: CSS anchor positioning with the shared try set (--flip-block, --flip-inline); without anchor support it opens centered in the top layer, still usable.
Fallback: browsers without invoker commands get togglePopover() on click from js/menu.js.