Skip to Content
Component ExamplesARIA Menus

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:

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" and aria-expanded to communicate state
  • Items inside role="menu" must have role="menuitem" and support Arrow key navigation
  • Escape must close the menu and return focus to the trigger button

role='menu' on Navigation vs. Plain Links with Disclosure

Inaccessible
<!-- 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>
Live Preview
Accessible
<!-- 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>
Live Preview

What’s wrong with role="menu" on navigation?

  • role="menubar" and role="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 with role="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:

VersionAnnouncement
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

Inaccessible
<!-- 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>
Live Preview
Accessible
<!-- 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>
Live Preview

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 only onclick handlers — they are invisible to screen readers and have no keyboard access
  • No aria-haspopup to indicate a popup menu exists
  • No aria-expanded to communicate open/closed state
  • No keyboard navigation (Arrow keys, Escape) is provided

What the screen reader announces:

VersionAnnouncement
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

ScenarioPatternWhy
Site navigation (header links, dropdowns)<nav> + <button aria-expanded> + <a> linksUsers 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> linksDisclosure 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

Resources