Some ARIA roles describe composite widgets — controls built from multiple coordinated parts. A role="tablist" is meaningless without role="tab" children. A role="listitem" is meaningless outside a role="list". Two audits enforce these relationships: aria-required-children fails when a parent role is missing the child roles it requires, and aria-required-parent fails when a child role appears outside its required parent.
aria-required-children (parent missing required child role) and aria-required-parent (child role outside its required parent).<ul>/<li>, <ol>/<li>, <select>/<option>, and <dl>/<dt>/<dd> handle the parent/child contract automatically.Think of composite widgets the way you think of a chess set: a board without pieces is just a checkered surface, and a knight off the board is just a horse-shaped object. Each only makes sense in relation to the other.
Composite-widget roles do not stand alone. Screen readers rely on the parent/child structure to compute the "1 of 5" counters, the "tab 2 of 4 selected" announcements, and the keyboard navigation patterns (arrow keys inside a tablist, Home and End inside a listbox). When the structure is broken, every one of those features breaks.
role="list" with no listitemchildren is announced as "list, empty" even when items are visible. The user cannot tell how many items are present.role="tab" outside a role="tablist" does not get arrow-key navigation. Keyboard users are stuck.role="option" outside a role="listbox" can announce aria-selected, but the user has no listbox to navigate, so the state is meaningless.role="treeitem" outside a role="tree" loses the hierarchy. Expand/collapse no longer makes sense.The WAI-ARIA specification defines the full list. The pairings most likely to fail an audit:
| Parent role | Required child role(s) | Native HTML equivalent |
|---|---|---|
list | listitem | <ul> / <ol> with <li> |
tablist | tab | (no native equivalent) |
menu / menubar | menuitem / menuitemcheckbox / menuitemradio | (no native equivalent) |
listbox | option (or grouped via group) | <select> / <option> |
radiogroup | radio | <input type="radio"> with shared name |
tree | treeitem (often grouped via group) | (no native equivalent) |
grid / treegrid / table | row with gridcell / columnheader / rowheader | <table> with <tr> and <td> / <th> |
combobox (with popup) | Popup listbox (or tree, grid, dialog) | <input> + <datalist> |
The relationship runs both ways. aria-required-children fails when a parent has no children of the required role; aria-required-parent fails when a child appears without the required ancestor.
The role="list" wrapper has visible bullet items, but each item is a <div>with no role. The screen reader announces "list, empty."
<!-- BAD -->
<div role="list">
<div>Apples</div>
<div>Bananas</div>
<div>Cherries</div>
</div>
<!-- GOOD: explicit listitem children -->
<div role="list">
<div role="listitem">Apples</div>
<div role="listitem">Bananas</div>
<div role="listitem">Cherries</div>
</div>
<!-- BEST: native semantics -->
<ul>
<li>Apples</li>
<li>Bananas</li>
<li>Cherries</li>
</ul>A horizontal strip of buttons styled as tabs but built without role="tab". Arrow-key navigation is broken and the strip is announced as a generic group.
<!-- BAD -->
<div role="tablist">
<button>Overview</button>
<button>Pricing</button>
<button>FAQ</button>
</div>
<!-- GOOD -->
<div role="tablist" aria-label="Plan details">
<button role="tab" aria-selected="true" aria-controls="p1" id="t1">Overview</button>
<button role="tab" aria-selected="false" aria-controls="p2" id="t2">Pricing</button>
<button role="tab" aria-selected="false" aria-controls="p3" id="t3">FAQ</button>
</div>A dropdown built with role="menu" that contains plain links. Screen readers announce the popup as a menu but find nothing inside it to navigate.
<!-- BAD -->
<ul role="menu">
<li><a href="/edit">Edit</a></li>
<li><a href="/delete">Delete</a></li>
</ul>
<!-- GOOD -->
<ul role="menu">
<li role="menuitem">Edit</li>
<li role="menuitem">Delete</li>
</ul>If the items are real navigation links rather than commands, use a <nav> with a list — role="menu" is for application-style menus only.
Custom select widgets sometimes wrap a list of <div> rows in role="listbox" without giving each row role="option". Selection state and arrow-key navigation both break.
<!-- BAD -->
<ul role="listbox">
<li>Small</li>
<li>Medium</li>
<li>Large</li>
</ul>
<!-- GOOD -->
<ul role="listbox" aria-label="Size">
<li role="option" aria-selected="false">Small</li>
<li role="option" aria-selected="true">Medium</li>
<li role="option" aria-selected="false">Large</li>
</ul>A row in a card grid is given role="listitem" with no role="list" wrapper. The role becomes orphaned and is dropped from the accessibility tree.
<!-- BAD -->
<div className="grid">
<article role="listitem">Card 1</article>
<article role="listitem">Card 2</article>
</div>
<!-- GOOD -->
<div role="list" className="grid">
<article role="listitem">Card 1</article>
<article role="listitem">Card 2</article>
</div>role="tab" requires role="tablist"as a direct ancestor. A single "tab" on its own makes no sense — there is nothing to switch between.
<!-- BAD -->
<button role="tab">Settings</button>
<!-- GOOD -->
<div role="tablist" aria-label="Account">
<button role="tab" aria-selected="true">Settings</button>
<button role="tab" aria-selected="false">Billing</button>
</div>A search-result row with role="option" sitting in a plain <div>. The option role implies single or multi-select — without a listbox parent, no selection model exists.
<!-- BAD -->
<div className="results">
<div role="option">First match</div>
<div role="option">Second match</div>
</div>
<!-- GOOD -->
<div role="listbox" aria-label="Search results">
<div role="option">First match</div>
<div role="option">Second match</div>
</div>File-row components are sometimes reused outside a tree (in a flat list, in a search-results panel) but keep role="treeitem". Expand/collapse semantics no longer apply.
Either wrap them in a role="tree" with the appropriate group sub-roles, or change the role on the row when it is rendered outside the tree. A reusable component should accept the role as a prop, not hardcode it.
aria-required-children and aria-required-parent, lists every parent missing required children and every orphan child role, and offers a one-click GitHub PR fix where applicable.<ul>, <ol>, <select>, <dl>, and <table> all wire up parent/child semantics for free. The First Rule of ARIA: if a native element exists, use it.
A role="list" wrapper means every visible row inside has to be a listitem. Half the rows with a role and half without is the most common aria-required-children failure.
role="group" when nesting is requiredTrees and listboxes that need sub-groups must wrap the nested set in role="group", not in another tree or listbox. A nested tree without group wrappers fails the parent audit.
role="list" accepts only listitem children. Putting a <button> as a direct child can throw both audits. Wrap the button in a listitem.
If your tabs do not switch panels with arrow keys, they are not ARIA tabs — they are buttons. Drop role="tablist" rather than fight the audit.
If items are loaded async and the parent renders before the children arrive, aria-required-children may flag the empty state. Either render a loading listitem or do not apply the parent role until the data arrives.
aria-required-parent looks at DOM ancestry. A role="treeitem" portaled out of its tree wrapper still fails. Keep composite widgets in one DOM subtree.
aria-controls wiringTab/panel and combobox/listbox pairs use aria-controls and aria-activedescendant to connect parts. React's useId() or any component-scoped ID hook prevents collisions across instances.
eslint-plugin-jsx-a11y rules including jsx-a11y/role-has-required-aria-props, jsx-a11y/role-supports-aria-props, and jsx-a11y/no-noninteractive-element-to-interactive-role catch many composite-widget mistakes before merge.
Listen for the "X of N" announcement when you arrow through items. Working composite widgets always announce position and count; broken ones go silent.
What's happening: A <ul> contains an animation wrapper <div> that contains the <li>. The browser's implicit list/listitem relationship is broken because <li> is no longer a direct child of <ul>.
Fix: Move the animation wrapper inside the <li>, or use role="list" on the wrapper and role="listitem" on the items so the relationship is explicit regardless of nesting.
What's happening: A FileRow component hardcodes role="treeitem". When the same component is used in a flat search-results list, it fails aria-required-parent.
Fix: Accept the role as a prop. Pass role="treeitem" from the tree and role="option" (with a listbox wrapper) from the search results.
What's happening: CSS display: contents on the parent flattens the layout, but the developer also changes the markup so the children are no longer DOM descendants of the role parent.
Fix: CSS layout does not affect the DOM tree, but moving elements with JavaScript or portals does. Keep the composite widget's parent and children in the same DOM subtree.
What's happening: role="list" renders before data arrives, fails aria-required-children, and the audit catches the empty state.
Fix: Render a placeholder role="listitem" with a loading message, or hold off on applying the parent role until at least one item exists.
It verifies that every element with a composite-widget role contains the child roles its parent role requires. A role="list" with no listitem children fails. A role="tablist" with no tab children fails.
It verifies that every child role appears inside its required parent role. A role="listitem" outside a role="list" fails. A role="tab" outside a role="tablist" fails.
Yes — native HTML provides the implicit roles. <ul> implies role="list" and <li> implies role="listitem" as long as <li> is a direct child of <ul>. Some browsers strip the implicit list role when the list is styled with list-style: none; if that affects your screen-reader testing, add an explicit role="list" to the <ul>.
Yes. It maps to Success Criterion 1.3.1 Info and Relationships (Level A) when the visual structure does not match the programmatic structure, and to 4.1.2 Name, Role, Value (Level A) when role announcements break.
Yes — that is exactly what role="listitem" exists for. But the parent must also have role="list" (or be a native <ul> / <ol>), or the orphan-child audit will fail.
The most common cause is the role being applied at the wrong level — role="tablist" on the outer container but the tabs nested inside an extra wrapping <div> that interrupts the parent/child relationship. Move the role to the direct parent of the tabs, or apply role="presentation" to the wrapper so the structure flattens.
Indirectly. AI answer engines such as Google AI Overviews, ChatGPT, and Perplexity parse the accessibility tree and semantic HTML to understand interactive structure. Broken composite widgets produce a noisier tree, weaker structured signals, and worse rankings in traditional Google search — and AI answer engines preferentially cite pages that already rank well.
aria-required-children and aria-required-parentare the two halves of the same contract: composite widgets only work when the parent role and child roles are paired. The fix is rarely "add more ARIA" — it is usually "use the native HTML element" or "apply the missing half of the pair so the contract is satisfied." Once the role hierarchy matches the visual hierarchy, every assistive-tech feature that depends on it (counters, arrow-key navigation, selection state) starts working again.
Run a Greadme deep scan to see exactly which composite widgets on your site are missing required children and which child roles are orphaned — then fix the component once and clear every instance at the same time.