OverlaysFlagship guide
Menu & Menu Button
A button that opens a menu of actions or links, navigable with arrow keys.
Last verified against WCAG 2.2 and WAI-ARIA APG 1.2 on 2026-07-02.
Implementation
menu-button-pattern.tsx
function MenuButtonPattern() {
const [open, setOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
const buttonRef = useRef(null);
const itemRefs = useRef([]);
function openMenu(focusIndex) {
setOpen(true);
setActiveIndex(focusIndex);
}
function closeMenu(restoreFocus) {
setOpen(false);
if (restoreFocus) buttonRef.current?.focus();
}
useEffect(() => {
if (open) itemRefs.current[activeIndex]?.focus();
}, [open, activeIndex]);
function onMenuKeyDown(e) {
if (e.key === "ArrowDown") { e.preventDefault(); setActiveIndex(i => (i + 1) % ITEMS.length); }
if (e.key === "ArrowUp") { e.preventDefault(); setActiveIndex(i => (i - 1 + ITEMS.length) % ITEMS.length); }
if (e.key === "Escape") { e.preventDefault(); closeMenu(true); }
// Menus aren't modal — Tab closes and continues to the next element.
if (e.key === "Tab") closeMenu(false);
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); closeMenu(true); }
}
return (
<>
<button
ref={buttonRef}
aria-haspopup="menu"
aria-expanded={open}
aria-controls="actions-menu"
onClick={() => (open ? closeMenu(false) : openMenu(0))}
onKeyDown={e => {
if (e.key === "ArrowDown" || e.key === "Enter") { e.preventDefault(); openMenu(0); }
if (e.key === "ArrowUp") { e.preventDefault(); openMenu(ITEMS.length - 1); }
}}
>
Actions
</button>
{open && (
<ul id="actions-menu" role="menu" aria-label="Actions" onKeyDown={onMenuKeyDown}>
{ITEMS.map((item, i) => (
<li key={item} role="none">
<button
ref={el => (itemRefs.current[i] = el)}
role="menuitem"
tabIndex={-1}
onClick={() => closeMenu(true)}
>
{item}
</button>
</li>
))}
</ul>
)}
</>
);
}Live demo
Required roles, states & properties
| Element | Attribute | Why |
|---|---|---|
| Trigger <button> | aria-haspopup="menu" | Tells AT this button opens a menu, so it can announce "has popup, menu" before it's even activated. |
| Trigger <button> | aria-expanded | Communicates the menu's open/closed state on the trigger itself. |
| Trigger <button> | aria-controls | Associates the trigger with the menu it opens, by id. |
| Menu container | role="menu" | Identifies the popup as a menu (not a generic list), enabling menu-specific AT navigation and the "menu" role announcement. |
| Each item | role="menuitem", tabIndex="-1" | Identifies each row as an actionable menu item; tabIndex=-1 keeps items out of the page Tab order — they're reached only via the menu's own arrow-key roving focus. |
Keyboard interaction model
| Key | Behavior |
|---|---|
| Enter / Space / Down Arrow (on button) | Opens the menu and moves focus to the first item. |
| Up Arrow (on button) | Opens the menu and moves focus to the last item. |
| Down / Up Arrow (in menu) | Moves focus to the next / previous item, wrapping at the ends. |
| Home / End | Moves focus to the first / last item. |
| Enter / Space (on item) | Activates the item and closes the menu, returning focus to the button. |
| Escape | Closes the menu and returns focus to the button. |
| Tab | Closes the menu (it is not modal) and moves focus to the next focusable element on the page. |
Focus management rules
- Opening via Down Arrow focuses the first item; via Up Arrow focuses the last item.
- Menu items use a roving tabindex (all -1) — arrow keys move real focus between them directly.
- Escape or activating an item closes the menu and restores focus to the trigger button.
- Tab closes the menu without restoring focus to the trigger — it continues the page's natural tab sequence, since menus are not modal.
WCAG 2.2 success criteria mapping
| SC | Name | Level | Why it applies |
|---|---|---|---|
| 2.1.1 | Keyboard | A | All functionality is operable through a keyboard interface with no specific timing. |
| 2.1.2 | No Keyboard Trap | A | Keyboard focus can always be moved away from a component using standard navigation. |
| 2.4.3 | Focus Order | A | Focusable components receive focus in an order that preserves meaning and operability. |
| 4.1.2 | Name, Role, Value | A | For all UI components, name, role, and value are programmatically determinable; states and changes are announced. |