The HTML aria-controls attribute
Use aria-controls to name, by id, the element a control changes, such as the panel behind a tab. It takes one id or a space-separated list. The link is information only, so your script still does the opening, closing or updating.
Overview
The aria-controls attribute points from a control to the element or elements whose contents or presence it changes. A table of contents that drives a document pane fits that description, and so do checkboxes that choose which prices a table tracks and a tab that shows its panel.
A button with aria-controls doesn't open its target when clicked, so that work stays with your script. In the accessibility tree the link appears as a relation, and the target stays under its DOM parent.
WAI-ARIA 1.2 requires it on the combobox and scrollbar roles. It's also required when aria-autocomplete is list or both, where it points at the element that holds the suggestions.
The target can start out hidden. Pointing a combobox at its popup before the popup is shown is valid, and easier to program than adding the link later. Neither Chrome's nor Firefox's tree shows a relation to a target with the hidden attribute until it's rendered.
A <details> element shows and hides its own content with no id to point at. For an input of a text type that has a list attribute, the HTML-AAM spec maps a matching aria-controls value, but neither browser's tree shows that link.
Syntax
<button aria-expanded="false" aria-controls="faq-1">Shipping</button>
<div id="faq-1" hidden>Orders leave the warehouse in two days.</div>
Values
| Value |
|---|
| A space-separated list of element id values. |
Example
<p style="margin:0 0 8px">Check the box. The script updates both elements named in aria-controls, because the attribute changes nothing by itself.</p>
<label><input id="archived" type="checkbox" aria-controls="projects total"> Show archived projects</label>
<ul id="projects" style="margin:8px 0">
<li>Website refresh</li>
<li>Mobile app</li>
<li hidden>Spring campaign</li>
<li hidden>Old blog</li>
</ul>
<p id="total" style="margin:8px 0">Showing 2 of 4 projects</p>
<p id="state" role="status" style="margin:8px 0 0">Nothing updated yet.</p>
<script>
const box = document.getElementById('archived');
const state = document.getElementById('state');
box.addEventListener('change', () => {
const ids = box.getAttribute('aria-controls').split(' ');
const [projects, total] = ids.map(id => document.getElementById(id));
const archived = [...projects.children].slice(2);
archived.forEach(item => { item.hidden = !box.checked; });
total.textContent = 'Showing ' + (box.checked ? 4 : 2) + ' of 4 projects';
state.textContent = 'The script updated #' + ids.join(' and #') + '.';
});
</script>
Best practices
- Pair
aria-controlswith aria-expanded on a button that shows and hides content. The first names the target, and the second reports whether it's open. - List every element one control changes, separated by spaces. A filter that updates a table and a result count would name both.
- Recheck ids after a re-render. A reference that matches no element is ignored, so a stale id fails without an error.
- Keep it for elements the control modifies. Help text that only explains the control isn't changed by it, so it belongs in aria-describedby.
- Point each
role="tab"at itstabpanelwith aria-controls, so every tab names the panel it shows.























