Who it affects
Someone using a screen reader lands on Copy link, menu item and has no idea a menu has opened, how many items it has, or that the arrow keys should move through it.
Someone using a screen reader in browse mode reads the items as loose text scattered in the page, with no start and end to the menu.
Someone using voice control may find the item is not treated as a menu command at all, so saying its name does nothing.
Broken versus fixed
Both examples are live and rendered in a sandboxed frame. The broken one is deliberately broken.
Looks like a menu. A screen reader announces: Copy link, menu item, with no menu around it, no 1 of 3, and no hint that arrow keys apply.
Announced: Share, menu, Copy link, menu item, 1 of 3. The arrow key handling is script you still need to write; the demo is static.
Show the markup behind both examples
<style>body{font-family:system-ui;padding:16px;margin:0}.btn{border:1px solid #cbd5e0;background:#fff;border-radius:6px;padding:6px 12px}.pop{margin-top:6px;width:180px;border:1px solid #cbd5e0;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.08);background:#fff}.pop div{padding:8px 12px}</style>
<button class="btn" aria-expanded="true" aria-haspopup="menu">Share</button>
<div class="pop">
<div role="menuitem" tabindex="0">Copy link</div>
<div role="menuitem" tabindex="-1">Email</div>
<div role="menuitem" tabindex="-1">Print</div>
</div>
<style>body{font-family:system-ui;padding:16px;margin:0}.btn{border:1px solid #cbd5e0;background:#fff;border-radius:6px;padding:6px 12px}.pop{margin-top:6px;width:180px;border:1px solid #cbd5e0;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.08);background:#fff}.pop div{padding:8px 12px}</style>
<button class="btn" id="share" aria-expanded="true" aria-haspopup="menu">Share</button>
<div class="pop" role="menu" aria-labelledby="share">
<div role="menuitem" tabindex="0">Copy link</div>
<div role="menuitem" tabindex="-1">Email</div>
<div role="menuitem" tabindex="-1">Print</div>
</div>
Test it by hand in two minutes
- Inspect the element with the child role and walk up its ancestors. Look for the parent role it needs (menu or menubar for menuitem, listbox for option, tablist for tab, tree for treeitem).
- If a wrapper sits between the child and the parent, check it has role="presentation" or role="none", or is a group the parent allows.
- Turn on VoiceOver (Cmd+F5) or NVDA and move to the item. You should hear the parent role first, or a position like 1 of 3.
- Press the arrow keys. In a real menu, listbox or tablist the screen reader should move through the items and announce each one.
The fix
<div>
<div role="menuitem">Copy link</div>
<div role="menuitem">Email</div>
</div>
<div role="menu" aria-label="Share">
<div role="menuitem">Copy link</div>
<div role="menuitem">Email</div>
</div>
Add the parent role to the element that wraps the items and give it a name. If the items are really just links in a dropdown, drop role="menuitem" and use a plain list of links instead; menu roles bring keyboard expectations with them.
Tab with no tablist
<nav>
<button role="tab" aria-selected="true">Details</button>
<button role="tab">Reviews</button>
</nav>
<div role="tablist" aria-label="Product">
<button role="tab" aria-selected="true">Details</button>
<button role="tab" aria-selected="false">Reviews</button>
</div>
A tab needs a tablist parent. The nav element is not one.
Options in a plain div
<div>
<div role="option">Small</div>
<div role="option">Large</div>
</div>
<div role="listbox" aria-label="Size">
<div role="option" aria-selected="true">Small</div>
<div role="option" aria-selected="false">Large</div>
</div>
Options need a listbox. If this is a dropdown in a form, a native select is simpler and already correct.
Common mistakes
- Putting the parent role on the grandparent and leaving an unlabelled div in between.
- Giving the parent role to the trigger button rather than the container of items.
- Removing the child role from every item to clear the finding, so the menu is now a pile of divs.
- Using role="menu" for site navigation; those are links, and a list of links is what screen readers expect there.
- Adding role="list" to the wrapper because a menu is a list, which is not the parent menuitem needs.
WCAG criteria this rule maps to
axe-core tags this rule as WCAG 2.2 level A. It is one of the checks for:
- 1.3.1 Info and Relationships (level A). Official W3C text.
The scanner can see a child role with no suitable parent in its ancestors, allowing for presentational wrappers. It cannot tell whether the menu or listbox actually works with a keyboard. Automated tools find only part of the accessibility problems on a page; a pass on this rule is not a pass on the criterion.
Related rules
aria-required-children
Some roles are containers that only mean something with the right children inside: listbox and option, tablist and tab, menu and menuitem, tree and treeitem, table and row.
A
ARIA roles must be valid
aria-roles
The role attribute only accepts names from the ARIA list: dialog, button, tab, listbox and so on.
A
List items must sit inside a list
listitem
An li is only a list item when its parent is a ul, ol or menu.
A
Do not put one control inside another
nested-interactive
A button, a tab, a switch and similar controls are announced as one thing; whatever is inside them is treated as their label, not as separate controls.