ARIA roles require correct parent roles for accessibility
Last updated:
Related Guides
ARIA roles must be inside their required parent roles
Child ARIA roles must be contained within the parent roles defined by the ARIA specification.
This appears in custom widgets like menus, lists, tabs, trees, and grids. Without the expected container, assistive tech cannot build the correct hierarchy or behavior, which confuses or blocks users.
Why It Matters
Screen readers and other assistive tech rely on role hierarchies to announce context and enable expected navigation. If a menu item is not inside a menu, the software may not provide arrow-key navigation or announce grouping.
Users who are blind or have low vision miss crucial context. Those using keyboards, switch controls, or voice control lose predictable focus movement. Cognitive load also increases when items seem unrelated or out of place.
Common Causes
- Using
role="menuitem"without a parent withrole="menu"orrole="menubar". - Using
role="option"outside arole="listbox"orrole="combobox"popup. - Placing
role="tab"outside arole="tablist", orrole="tabpanel"without linkage. - Putting
role="treeitem"outside arole="tree"(or required grouping within a tree). - Using
role="row"without a table/grid context (e.g.,role="table", "grid", or a proper rowgroup). - Radios with
role="radio"not grouped byrole="radiogroup". - Attempting to “fake” structure with
aria-ownsinstead of fixing the DOM order.
How to Fix
- Identify required context
- Look up each ARIA role in the WAI-ARIA spec to find its required parent/container role.
- Common pairs:
- menuitem → inside menu or menubar
- tab → inside tablist; tabpanel linked to its tab
- option → inside listbox (or the popup list for a combobox)
- treeitem → inside tree (may be grouped by
role="group"within the tree) - radio → inside radiogroup
- row → inside table/grid/treegrid (often within rowgroup); cells/headers inside row
- Fix the DOM, not just attributes
- Place each child role element inside the correct parent element.
- If you cannot change markup structure, reconsider the widget design. Avoid ARIA if native elements can do the job.
- Link related parts
- Use
aria-labelledbyto provide programmatic labels. - Use
aria-controlsto associate controllers (e.g., tabs) with controlled regions (e.g., panels). - For composite widgets that keep focus on the container, use
aria-activedescendantto point to the active child. - Use
aria-posinsetandaria-setsizeto convey position/count for items in a set when needed. - Use
aria-ownssparingly; it can re-parent in the accessibility tree but should not replace proper DOM nesting.
- Use
- Match behavior to roles
- Implement expected keyboard patterns (e.g., arrow keys within menus and tablists, Tab to enter/exit the widget).
- Keep DOM order aligned with visual/reading order; use
aria-flowtoonly in rare, well-justified cases.
- Validate
- Inspect the accessibility tree in browser devtools to confirm parent-child role relationships.
- Run automated checks, then manually verify hierarchy and behavior.
How to Test
Keyboard check
- Tab to the widget. Arrow keys move between items as expected (e.g., tabs, menu items).
- Tab exits the widget appropriately. Focus is always visible.
Screen reader check
- With NVDA/JAWS/VoiceOver, navigate into the widget.
- The container role is announced (e.g., “tablist”, “menu”, “listbox”).
- Items are announced with their role and state (e.g., “tab selected”, “menu item”).
- Relationships work: tabs announce their panel, and panels announce their controlling tab.
Mobile/touch check
- With VoiceOver or TalkBack, swipe through the widget.
- Containers are recognized (rotor/quick nav shows the structure). Items read in order with correct roles.
Quick programmatic check
- Open the browser’s Accessibility/ARIA tree and confirm that each child role is nested under the correct parent.
Good Example
<div role="tablist" aria-label="Billing sections">
<button role="tab" id="tab-overview" aria-selected="true" aria-controls="panel-overview">Overview</button>
<button role="tab" id="tab-invoices" aria-selected="false" aria-controls="panel-invoices" tabindex="-1">Invoices</button>
</div>
<section role="tabpanel" id="panel-overview" aria-labelledby="tab-overview">Overview content
</section>
<section role="tabpanel" id="panel-invoices" aria-labelledby="tab-invoices" hidden>
Invoices content
</section>
Why this is good
- Tabs are inside a tablist (required parent).
- Each tab is linked to its panel via
aria-controls; each panel references its tab viaaria-labelledby. - Selection state and focus order are managed.
Bad Example
<button role="tab" id="t1">Overview</button>
<button role="tab" id="t2">Invoices</button>
<section role="tabpanel" aria-labelledby="t1">Overview content</section>
<section role="tabpanel" aria-labelledby="t2">Invoices content</section>What’s wrong
role="tab"elements are not inside arole="tablist".- Panels are present, but the missing parent role prevents correct semantics and keyboard behavior.
Quick Checklist
- Every ARIA role is placed inside its required parent role.
- DOM nesting matches the semantic hierarchy; avoid using
aria-ownsto compensate. - Use native elements when possible; add ARIA only when necessary.
- Link related parts with
aria-labelledbyandaria-controls; usearia-activedescendantfor composite widgets. - Provide expected keyboard support for the chosen pattern.
- Verify the role hierarchy in the accessibility tree.
- Automated tests pass, and manual screen reader checks confirm correct announcements.