A11y menu
Web accessibility agent skills — 23 cite-backed skills covering APG widget patterns, audit tooling, ARIA guidance, cognitive accessibility, and more. Works with Claude Code, Codex CLI, and Gemini CLI.
npx -y skills add xrnavigation/web-a11y-plugin --skill a11y-menuAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Guides accessible application menu and menu button implementation per APG patterns. Auto-invokes when creating menus, menubars, context menus, action dropdowns, or menu buttons. Critical distinction — site navigation is NOT a menu. Covers menu/menubar/menuitem roles, menuitemcheckbox, menuitemradio, and the menu button pattern.
SKILL.md
14.3 KB, as published. Nobody here has run it
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 ${CLAUDE_SKILL_DIR}/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 ${CLAUDE_SKILL_DIR}/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:
${CLAUDE_SKILL_DIR}/references/menu-vs-navigation.md— full decision criteria${CLAUDE_SKILL_DIR}/references/keyboard-interaction.md— complete keyboard spec${CLAUDE_SKILL_DIR}/references/menu-button-pattern.md— menu button implementation details${CLAUDE_SKILL_DIR}/references/common-mistakes.md— anti-patterns with code examples${CLAUDE_SKILL_DIR}/references/sources.yaml— provenance for all cited sources