Accessible Menus Pattern
APG menu and menubar keyboard model — arrow navigation, escape to close, aria-expanded, and when a disclosure is enough.
- accessibility
- accessible-menus
“Menu” in UI kits often means a dropdown of actions. In ARIA, menu / menuitem imply a specific keyboard interaction model (arrow keys, not just Tab through every item). Misusing role="menu" on site navigation is a common antipattern.
Docs: APG Menu, APG Menubar, Navigation menus.
Menu vs navigation vs disclosure
| UI | Prefer |
|---|---|
| Site primary nav links | <nav> + links (and disclosure for mobile) |
| Application action menu (Edit, Delete) | menu pattern |
| Simple show/hide extra links | Disclosure button + aria-expanded |
Don’t put role="menu" on a list of <a href> site links — screen reader users expect navigation landmarks, not application menus.
Action menu skeleton
<button
type="button"
id="actions-btn"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="actions-menu"
>
Actions
</button>
<ul id="actions-menu" role="menu" hidden aria-labelledby="actions-btn">
<li role="presentation">
<button type="button" role="menuitem">Edit</button>
</li>
<li role="presentation">
<button type="button" role="menuitem">Duplicate</button>
</li>
<li role="presentation">
<button type="button" role="menuitem">Delete</button>
</li>
</ul>
Keyboard expectations (application menu)
| Key | Behavior |
|---|---|
| Enter/Space on button | Open menu; focus first item |
| Down/Up | Move between menuitems |
| Home/End | First/last |
| Escape | Close; return focus to button |
| Tab | Typically closes and moves focus (per APG) |
| Character | Typeahead optional |
Use roving tabindex: only the active menuitem has tabindex="0", others -1.
Focus management
- Store opener.
- Open → focus first item.
- Close → focus opener.
See focus management.
Submenus
Menubar + submenu patterns add Right/Left arrows. Complexity climbs fast — use a library or follow APG meticulously.
Footguns
role="menu"on site nav.- Mouse hover open only.
- Focus lost on close.
divmenuitems without keyboard activation.- Missing
aria-expandedon the opener.
Interview out-loud answer
“I distinguish navigation links from application menus. True menus use arrow-key navigation, aria-expanded on the button, and restore focus on Escape. For simple nav, I use links and disclosures instead of role=menu.”
Roving tabindex sketch
const items = [...menu.querySelectorAll('[role="menuitem"]')];
let index = 0;
function setActive(i) {
items.forEach((el, j) => {
el.tabIndex = j === i ? 0 : -1;
});
items[i].focus();
index = i;
}
menu.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
setActive((index + 1) % items.length);
}
if (e.key === 'ArrowUp') {
e.preventDefault();
setActive((index - 1 + items.length) % items.length);
}
if (e.key === 'Escape') {
closeMenu();
opener.focus();
}
});
Separators and disabled items
<li role="separator"></li>
<button type="button" role="menuitem" aria-disabled="true">Rename</button>
Disabled items must not activate. Pick one APG behavior for whether arrows land on them and stick to it.
Outside click and focus
Mouse users expect click-outside to close. Restore focus to the opener on Escape always; on outside click, restoring to the opener is usually still correct so keyboard users aren’t stranded.
Mobile and touch
Application menus on touch devices often open on press and close on outside tap. Keep the same ARIA state updates (aria-expanded) as keyboard. Ensure hit targets meet size guidance so menuitems aren’t tiny text-only rows.
Related on this site
- Accessible modals patterns
- Accessible combobox pattern
- Focus management
- ARIA roles overview
- Keyboard accessibility checklist
Further reading
Related guides
- Accessible Combobox PatternBuild or evaluate comboboxes with APG keyboard behavior, aria-expanded/activedescendant, and filterable listbox pairing.
- Accessible Forms ErrorsLabel inputs, associate errors with fields, announce failures, and avoid placeholder-only forms that break accessibility.
- Accessible Modals PatternsModal dialogs that work: focus trap, Escape, return focus, aria-modal, and why native dialog or APG patterns beat div soup.
- Accessible Names ComputationHow browsers compute accessible names from content, labels, aria-label, and labelledby — and how that powers Testing Library queries.
- Accessible SVGsDecorative vs informative SVG icons — aria-hidden, title/desc, role=img, and pairing icons with accessible names on controls.