Overview
The ARIA role="menu" pattern is one of the most misused ARIA roles on the web. It is designed exclusively for application-style action menus — the kind you find in desktop software (File, Edit, View). When a screen reader encounters role="menu", it enters a special interaction mode: Tab is swallowed, Arrow keys navigate between items, and typing a letter jumps to matching items. This is exactly what you want for an application toolbar action menu, but it is catastrophic for website navigation where users expect Tab to move between links. If you are building site navigation, use the disclosure pattern instead. Reserve role="menu" for action dropdowns, context menus, and toolbar menus.
WCAG Criteria:
- 4.1.2 Name, Role, Value — all UI components must expose correct role, name, and state
- 2.1.1 Keyboard — all functionality must be available from a keyboard
Key requirements:
- Never use
role="menu"for site navigation — it changes screen reader interaction mode and breaks Tab - Site navigation should use
<nav>,<ul>,<a>links, and<button aria-expanded>for dropdowns - Use
role="menu"only for application-style action menus (e.g., Edit, Duplicate, Delete actions) - Menu buttons use
aria-haspopup="menu"andaria-expandedto communicate state - Items inside
role="menu"must haverole="menuitem"and support Arrow key navigation - Escape must close the menu and return focus to the trigger button
Navigation Misuse
role='menu' on Navigation vs. Plain Links with Disclosure
<!-- role="menu" on site nav — breaks Tab, confuses screen readers -->
<nav>
<ul role="menubar">
<li role="none">
<a role="menuitem" href="#" aria-haspopup="true"
aria-expanded="false">Services</a>
<ul role="menu">
<li role="none">
<a role="menuitem" href="/consulting">Consulting</a>
</li>
<li role="none">
<a role="menuitem" href="/development">Development</a>
</li>
<li role="none">
<a role="menuitem" href="/support">Support</a>
</li>
</ul>
</li>
<li role="none">
<a role="menuitem" href="/blog">Blog</a>
</li>
<li role="none">
<a role="menuitem" href="/contact">Contact</a>
</li>
</ul>
</nav><!-- Disclosure pattern — Tab works, links announced correctly -->
<nav aria-label="Main">
<ul>
<li>
<button aria-expanded="false"
onclick="
var expanded = this.getAttribute('aria-expanded') === 'true';
this.setAttribute('aria-expanded', String(!expanded));
var panel = document.getElementById('services-panel');
if (!expanded) { panel.removeAttribute('hidden'); }
else { panel.setAttribute('hidden', ''); }
">
Services
</button>
<ul id="services-panel" hidden
onkeydown="
if (event.key === 'Escape') {
this.setAttribute('hidden', '');
var btn = this.previousElementSibling;
btn.setAttribute('aria-expanded', 'false');
btn.focus();
}
">
<li><a href="/consulting">Consulting</a></li>
<li><a href="/development">Development</a></li>
<li><a href="/support">Support</a></li>
</ul>
</li>
<li><a href="/blog">Blog</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>What’s wrong with role="menu" on navigation?
role="menubar"androle="menu"tell screen readers this is an application menu — JAWS and NVDA enter a mode where Tab is swallowed and Arrow keys are expected<a>links withrole="menuitem"lose their link announcement — screen readers say “menu item” instead of “link”- Users who expect Tab to move between navigation links will appear to be stuck
- The trigger is a link (
<a>) acting as a toggle button, which is semantically incorrect
What the screen reader announces:
| Version | Announcement |
|---|---|
| Inaccessible (role=“menu”) | “Services, menu item. Menu. Consulting, menu item.” — Tab does not work, Arrow keys expected |
| Accessible (disclosure) | “Main, navigation. Services, collapsed, button” — on activate: “Services, expanded, button. Consulting, link. Development, link. Support, link.” |
Application Action Menu
Unsemantic Action Dropdown vs. Proper ARIA Menu
<!-- Div trigger, div items — no roles, no keyboard support -->
<div onclick="togglePanel()">Actions</div>
<div id="action-panel" style="display:none">
<div onclick="edit()">Edit</div>
<div onclick="duplicate()">Duplicate</div>
<div onclick="remove()">Delete</div>
</div><!-- Proper ARIA menu — this IS the correct use case for role="menu" -->
<button
aria-haspopup="menu"
aria-expanded="false"
onclick="
var expanded = this.getAttribute('aria-expanded') === 'true';
this.setAttribute('aria-expanded', String(!expanded));
var panel = document.getElementById('action-menu');
if (!expanded) {
panel.removeAttribute('hidden');
panel.querySelector('[role=menuitem]').focus();
} else {
panel.setAttribute('hidden', '');
}
"
>
Actions
</button>
<ul id="action-menu" role="menu" hidden
onkeydown="
var items = this.querySelectorAll('[role=menuitem]');
var idx = Array.prototype.indexOf.call(items, document.activeElement);
if (event.key === 'ArrowDown') {
event.preventDefault();
items[(idx + 1) % items.length].focus();
} else if (event.key === 'ArrowUp') {
event.preventDefault();
items[(idx - 1 + items.length) % items.length].focus();
} else if (event.key === 'Escape') {
this.setAttribute('hidden', '');
/* return focus to trigger */
this.previousElementSibling.focus();
}
"
>
<li role="none">
<button role="menuitem">Edit</button>
</li>
<li role="none">
<button role="menuitem">Duplicate</button>
</li>
<li role="none">
<button role="menuitem">Delete</button>
</li>
</ul>What’s wrong with the unsemantic action dropdown?
- The trigger is a
<div>— it has no button role, cannot receive focus with Tab, and cannot be activated with Enter or Space - The action items are
<div>elements with onlyonclickhandlers — they are invisible to screen readers and have no keyboard access - No
aria-haspopupto indicate a popup menu exists - No
aria-expandedto communicate open/closed state - No keyboard navigation (Arrow keys, Escape) is provided
What the screen reader announces:
| Version | Announcement |
|---|---|
| Inaccessible (divs) | “Actions” (just text, no role) — items are not discoverable by keyboard or screen reader |
| Accessible (ARIA menu) | “Actions, menu, collapsed, button” — on activate: “Actions, menu, expanded, button. Edit, menu item.” — Arrow keys move between items, Escape closes |
When to Use role="menu" vs. Disclosure
| Scenario | Pattern | Why |
|---|---|---|
| Site navigation (header links, dropdowns) | <nav> + <button aria-expanded> + <a> links | Users expect Tab navigation; menu roles break it |
| Action dropdown (Edit, Delete, Duplicate) | <button aria-haspopup="menu"> + role="menu" + role="menuitem" | Application-style actions that do not navigate — Arrow keys are expected |
| Context menu (right-click actions) | role="menu" + role="menuitem" | Mirrors OS context menu behavior |
| Toolbar overflow (more actions) | role="menu" + role="menuitem" | Subset of toolbar actions in a dropdown |
| Mega menu (large nav panels) | <button aria-expanded> + panel of <a> links | Disclosure pattern — NO menu roles |
Key teaching points:
role="menu"changes the screen reader’s interaction mode — it swallows Tab and expects Arrow key navigation- Site navigation should NEVER use
role="menu"— the disclosure pattern (<button aria-expanded>+ panel) is the APG-recommended approach - When
role="menu"IS appropriate, implement full keyboard support: ArrowUp/ArrowDown to move, Escape to close, Home/End to jump, and type-ahead letter navigation - Menu items should be
<button role="menuitem">for actions, or<a role="menuitem">only if they truly navigate away