aria-controls
Names the element or elements whose presence or content this control governs. Common pairings: a tab controls its tabpanel, a disclosure button controls a revealed region. Support across assistive tech is uneven — use sparingly.
When to use
On a control whose activation changes a different element on the page — the canonical examples are the tab → tabpanel relationship and a disclosure button → revealed panel. It tells assistive tech that “operating this widget will affect that one over there”, giving advanced users a way to jump from the control to the controlled element.
Do not reach for it on every interaction. Most click handlers do not need aria-controls; reserve it for cases where the controlled element is genuinely separate in the DOM and the user benefits from knowing the connection.
How it behaves
The value is a space-separated list of element IDs for the controlled element(s). The controlled element must exist in the DOM at the time the relationship is queried — if you mount the tabpanel only when the tab is activated, the reference dangles for sighted-AT users until the panel appears.
Support is the soft spot. JAWS exposes a “go to controlled element” shortcut, but VoiceOver and NVDA largely ignore the attribute outside specific patterns (tabs, comboboxes). That means aria-controls is rarely the thing that makes a feature accessible — it is an enhancement on top of correct roles, focus management, and aria-expanded. Build the pattern right first; add aria-controls last.
Common failures
- Treating
aria-controlsas a substitute for moving focus. A “Skip to results” button needs to move focus on activation; addingaria-controlsdoes not do that. - Pointing at an element that is not yet in the DOM. The reference resolves to nothing until the panel renders.
- Forgetting to also set
aria-expandedon a disclosure button.aria-controlssays “I influence that thing”;aria-expandedsays “and it is currently open / closed”. - Using
aria-controlson a custom select to point at an option list — the right pattern there isrole="combobox"plusaria-controlson the input, witharia-activedescendantfor the highlighted option. - Sprinkling
aria-controlson every button “for completeness”. Noise; nothing relies on it.
Example
<button
type="button"
aria-expanded="false"
aria-controls="faq-1-answer"
>
What is your refund policy?
</button>
<div id="faq-1-answer" hidden>
Refunds are processed within 14 days …
</div>