Accessible Menu Patterns
Site navigation is NOT a menu. If you are building navigation links, stop here. Use
<nav>with a list of links. See Section 1.
"Generally, don't use
menu,menuitem,menubar,menuitemcheckbox, ormenuitemradio. Only if you build something like Google Docs... these are warranted." — Marco Zehe, Mozilla (cited in Roselli, 2017)
1. Navigation Is NOT a Menu
This is the most common mistake in menu accessibility. ARIA menu/menubar/menuitem roles exist exclusively for application-style menus — the kind in desktop software (File, Edit, View). They are never for website navigation.
Why role="menu" Breaks Navigation
Screen readers switch interaction modes. JAWS enters forms mode; NVDA enters focus mode. Arrow keys stop reading content and are intercepted for menu navigation. Users can no longer browse the page normally. (Tink.uk; Accessible Culture)
Keyboard expectations change completely.
role="menu"commits you to implementing Enter, Space, Down/Up/Left/Right Arrow, Home, End, Escape, and character-key navigation. Most site navs implement none of these. Users hear "menu," try menu keyboard commands, and nothing works. (Roselli, 2017)Navigation disappears from landmarks and link lists. The
<nav>landmark is replaced by a menu widget. Links givenrole="menuitem"vanish from the screen reader links list (Insert+F7 in JAWS). Users lose two primary ways to discover site structure. (Roselli, 2017)Broken parent-child relationships.
<ul role="menu">withoutrole="presentation"on<li>androle="menuitem"on<a>creates orphaned elements, violating WCAG SC 1.3.1. (Make Things Accessible)
Wrong vs Right
<!-- WRONG — navigation is not an application menu -->
<nav>
<ul role="menu">
<li role="menuitem"><a href="/about">About</a></li>
<li role="menuitem"><a href="/news">News</a></li>
</ul>
</nav>
<!-- RIGHT — semantic navigation, no ARIA needed -->
<nav aria-label="Main">
<ul>
<li><a href="/about">About</a></li>
<li><a href="/news">News</a></li>
</ul>
</nav>
For collapsible/dropdown navigation, use the disclosure pattern (<button> + aria-expanded), not menu roles. See Roselli, "Link + Disclosure Widget Navigation" (2019).
When to Use Menu Roles
Use menu/menubar/menuitem only when all of these are true:
- You are building an application (not a content website)
- Items are actions or functions (not links to other pages)
- You will implement the complete keyboard interaction model
- The pattern mirrors desktop application menus
- You want screen readers to enter forms/focus mode
Examples: rich text editor toolbar, email client actions, IDE menu bar, drawing app context menu.
2. Menu Button Pattern
The menu button is the most common legitimate use of ARIA menus. A button opens a popup menu of actions. (APG Menu Button)
Required Structure
<button
aria-haspopup="menu"
aria-expanded="false"
aria-controls="actions-menu"
>
Actions
</button>
<ul id="actions-menu" role="menu" aria-label="Actions" hidden>
<li role="menuitem" tabindex="-1">Cut</li>
<li role="menuitem" tabindex="-1">Copy</li>
<li role="menuitem" tabindex="-1">Paste</li>
<li role="separator"></li>
<li role="menuitem" tabindex="-1">Delete</li>
</ul>
Button Requirements
| Property | Value |
|---|---|
| Element | <button> (native, not <div>) |
aria-haspopup |
"menu" (never changes) |
aria-expanded |
"true" when open, "false" when closed |
aria-controls |
ID of the menu element (optional but recommended) |
Button Keyboard Interaction
| Key | Action |
|---|---|
| Enter / Space | Opens menu, focuses first item |
| Down Arrow | (Optional) Opens menu, focuses first item |
| Up Arrow | (Optional) Opens menu, focuses last item |
Focus Management
- Open: focus moves to the first menu item (or the currently checked item for persistent selections — Pickering)
- Close (Escape or activation): focus returns to the button
- Tab: exits the menu and closes it
Screen Reader Announcements
VoiceOver announces aria-haspopup values differently: "menu" produces "menu pop-up, button" while "true" produces "menu button, group." JAWS adds: "Press Space to activate the menu. Then navigate with arrow keys." (Matuzovic, 2023)
3. Required ARIA Structure
Menu / Menubar Skeleton
<!-- Menubar (persistent, horizontal) -->
<div role="menubar" aria-label="Text Editor">
<!-- Top-level item with submenu -->
<div role="menuitem" aria-haspopup="menu" aria-expanded="false" tabindex="0">
File
<ul role="menu" aria-label="File">
<li role="menuitem" tabindex="-1">New</li>
<li role="menuitem" tabindex="-1">Open...</li>
<li role="menuitem" tabindex="-1">Save</li>
<li role="separator"></li>
<li role="menuitem" tabindex="-1">Exit</li>
</ul>
</div>
<div role="menuitem" aria-haspopup="menu" aria-expanded="false" tabindex="-1">
Edit
<ul role="menu" aria-label="Edit">
<li role="menuitem" tabindex="-1">Undo</li>
<li role="menuitem" tabindex="-1">Redo</li>
</ul>
</div>
</div>
Key Rules
- Roving tabindex: first menubar item gets
tabindex="0", all others gettabindex="-1". Movetabindex="0"as focus changes. (APG Menubar) - Accessible name: every
menuandmenubarneedsaria-labeloraria-labelledby. (MDN; BOIA) - Valid children only:
menu/menubarmay only containmenuitem,menuitemcheckbox,menuitemradio,group, orseparator. (MDN) - Disabled items: use
aria-disabled="true"— items remain focusable but cannot be activated. (APG Menubar) - Submenu items: add
aria-haspopup="menu"andaria-expandedto any menuitem that opens a submenu. - Avoid dual-purpose items: a menuitem should not both execute a function AND open a submenu. (APG Menubar)
- Dialog items: append ellipsis ("...") to items that open dialogs.
Required States and Properties
| Property | Where | Value |
|---|---|---|
aria-haspopup |
Menuitem with submenu | "menu" |
aria-expanded |
Menuitem with submenu | "true" / "false" |
aria-checked |
menuitemcheckbox / menuitemradio |
"true" / "false" (+ "mixed" for checkbox) |
aria-disabled |
Disabled items | "true" |
aria-label / aria-labelledby |
menu / menubar |
Accessible name |
tabindex |
First menubar item: 0; all others: -1 |
Roving tabindex |
4. Menuitemcheckbox and Menuitemradio
menuitemcheckbox
A checkable menu item with three possible states. (MDN)
<ul role="menu" aria-label="View options">
<li role="menuitemcheckbox" aria-checked="true" tabindex="-1">Show toolbar</li>
<li role="menuitemcheckbox" aria-checked="false" tabindex="-1">Show status bar</li>
<li role="menuitemcheckbox" aria-checked="mixed" tabindex="-1">Show rulers</li>
</ul>
aria-checked:"true","false", or"mixed"(indeterminate)- Enter toggles checked state and closes the menu
- Space toggles checked state and keeps the menu open (allows toggling multiple items)
- All descendants are presentational — semantic elements inside lose their semantics
menuitemradio
A mutually exclusive option within a group. (MDN)
<ul role="menu" aria-label="Text size">
<li role="group" aria-label="Font size">
<li role="menuitemradio" aria-checked="false" tabindex="-1">Small</li>
<li role="menuitemradio" aria-checked="true" tabindex="-1">Medium</li>
<li role="menuitemradio" aria-checked="false" tabindex="-1">Large</li>
</li>
</ul>
aria-checked:"true"or"false"only (no"mixed")- When activated, set own
aria-checked="true"and all siblings' to"false" - Groups: use
grouprole orseparatorto define separate radio groups within the same menu - Cannot contain interactive content or elements with
tabindex
Visual Indicators (CSS)
Use CSS pseudo-elements — avoids adding DOM content that screen readers would announce redundantly. (MDN)
[role="menuitemcheckbox"][aria-checked="true"]::before { content: "\2713"; }
[role="menuitemradio"][aria-checked="true"]::before {
background-color: currentColor;
border-radius: 50%;
}
5. Keyboard Interaction Summary
Full keyboard model is in references/keyboard-interaction.md.
| Key | In Menubar | In Menu (Popup/Submenu) |
|---|---|---|
| Enter | Opens submenu (first item) | Activates item, closes menu |
| Space | Opens submenu (first item) | Checkbox/radio: toggles (menu stays open). Other: activates, closes |
| Down Arrow | Opens submenu (first item) | Next item |
| Up Arrow | (Optional) Opens submenu (last item) | Previous item |
| Right Arrow | Next menubar item | Opens submenu / moves right in menubar |
| Left Arrow | Previous menubar item | Closes submenu / moves left in menubar |
| Escape | — | Closes menu, returns focus to invoker |
| Home / End | First / last menubar item | First / last item in current menu |
Critical: if you add role="menu", you must implement all of these. Partial implementation is worse than none — users hear "menu," try the expected keys, and nothing works. (Roselli, 2017; BOIA)
6. Common Mistakes
Detailed examples in references/common-mistakes.md.
1. Using role="menu" for site navigation
The most widespread mistake. Triggers forms/focus mode, breaks Tab navigation, requires unimplemented keyboard handling. (Roselli, 2017; Make Things Accessible)
2. Incomplete ARIA role application
Adding role="menu" to <ul> without role="presentation" on <li> and role="menuitem" on children. Creates orphaned elements, violates SC 1.3.1. (Make Things Accessible)
3. Making links into menuitems
role="menuitem" on <a> strips link semantics. Screen readers no longer announce links; items vanish from link lists. If items are navigation destinations, they must be links, not menuitems. (Pickering; Roselli, 2017)
4. Missing accessible name on menu
A menu requires aria-label or aria-labelledby. Without it, screen reader users cannot identify the menu's purpose. (MDN; BOIA)
5. Missing keyboard interaction
Adding ARIA menu roles without implementing arrow keys, Home, End, Escape, character navigation. The roles promise behavior that is absent. Roselli: incorrect ARIA nesting "can wreak havoc and make your site navigation completely unusable." (Roselli, 2017)
6. Using the HTML <menu> element
The HTML <menu> element is deprecated and maps to <ul> semantics. The <menuitem> HTML element is also deprecated. No modern browser supports them reliably. These are unrelated to ARIA menu roles. Roselli: "Do not use them." (Roselli, 2023)
7. Cross-References
aria-decision-framework— use the decision tree before reaching for any ARIA role. Menu roles are Step 3 cases — only when no native HTML equivalent exists.a11y-combobox— if users are selecting a value (not executing an action), usecomboboxorlistbox, notmenu.a11y-dialog— menuitems that open dialogs should append "..." to their label.
For detailed reference material:
- references/menu-vs-navigation.md — full decision criteria
- references/keyboard-interaction.md — complete keyboard spec
- references/menu-button-pattern.md — menu button implementation details
- references/common-mistakes.md — anti-patterns with code examples
- references/sources.yaml — provenance for all cited sources