aria-controls
Identifies the element (or elements) whose contents or presence are controlled by the current element. Creates a programmatic relationship between a control and the element it affects.
Overview
The aria-controls attribute identifies the element (or elements) whose contents or presence are controlled by the element on which the attribute is set. It creates a programmatic relationship that assistive technologies can expose to users.
The value is a space-separated list of one or more ID references pointing to the controlled elements. This is commonly used with buttons that expand/collapse content, tabs that show/hide panels, and other interactive controls.
Note: While aria-controls creates a relationship, not all screen readers actively announce it. It's still valuable for programmatic discovery and should be used alongside aria-expanded for disclosure patterns.
Live Demo: Collapsible Panel & Tabs
Collapsible Panel
aria-expanded="false" aria-controls="demo-panel"
Tab Navigation
This is the Overview panel content. Each tab button has aria-controls pointing to its panel.
aria-controls links the control (button/tab) to the element it affects. This relationship helps assistive technologies understand which content changes when the control is activated.
Code Examples
Basic Usage
<!-- aria-controls identifies the element being controlled -->
<!-- Button controls a collapsible panel -->
<button
aria-expanded="false"
aria-controls="details-panel"
onclick="togglePanel()"
>
Show Details
</button>
<div id="details-panel" hidden>
<p>This is the controlled content panel.</p>
</div>
<!-- The aria-controls value matches the id of the controlled element -->Accordion Pattern
<!-- Accordion with aria-controls -->
<div class="accordion">
<!-- Accordion item 1 -->
<h3>
<button
aria-expanded="true"
aria-controls="section1-content"
id="section1-header"
>
Section 1
</button>
</h3>
<div
id="section1-content"
role="region"
aria-labelledby="section1-header"
>
<p>Content for section 1...</p>
</div>
<!-- Accordion item 2 -->
<h3>
<button
aria-expanded="false"
aria-controls="section2-content"
id="section2-header"
>
Section 2
</button>
</h3>
<div
id="section2-content"
role="region"
aria-labelledby="section2-header"
hidden
>
<p>Content for section 2...</p>
</div>
</div>Tab Pattern
<!-- Tabs with aria-controls -->
<div role="tablist" aria-label="Product information">
<button
role="tab"
id="tab-details"
aria-selected="true"
aria-controls="panel-details"
tabindex="0"
>
Details
</button>
<button
role="tab"
id="tab-specs"
aria-selected="false"
aria-controls="panel-specs"
tabindex="-1"
>
Specifications
</button>
<button
role="tab"
id="tab-reviews"
aria-selected="false"
aria-controls="panel-reviews"
tabindex="-1"
>
Reviews
</button>
</div>
<div
role="tabpanel"
id="panel-details"
aria-labelledby="tab-details"
>
<p>Product details content...</p>
</div>
<div
role="tabpanel"
id="panel-specs"
aria-labelledby="tab-specs"
hidden
>
<p>Specifications content...</p>
</div>
<div
role="tabpanel"
id="panel-reviews"
aria-labelledby="tab-reviews"
hidden
>
<p>Customer reviews content...</p>
</div>React Components
// React components with aria-controls
import { useState, useId } from 'react';
// Collapsible Panel Component
function CollapsiblePanel({ title, children }) {
const [isOpen, setIsOpen] = useState(false);
const panelId = useId();
return (
<div className="collapsible">
<button
aria-expanded={isOpen}
aria-controls={panelId}
onClick={() => setIsOpen(!isOpen)}
>
{title}
<span aria-hidden="true">{isOpen ? '▼' : '▶'}</span>
</button>
<div
id={panelId}
hidden={!isOpen}
role="region"
>
{children}
</div>
</div>
);
}
// Tabs Component with aria-controls
function Tabs({ tabs }) {
const [activeTab, setActiveTab] = useState(0);
const baseId = useId();
return (
<div>
<div role="tablist">
{tabs.map((tab, index) => {
const tabId = `${baseId}-tab-${index}`;
const panelId = `${baseId}-panel-${index}`;
return (
<button
key={index}
role="tab"
id={tabId}
aria-selected={activeTab === index}
aria-controls={panelId}
tabIndex={activeTab === index ? 0 : -1}
onClick={() => setActiveTab(index)}
onKeyDown={(e) => handleTabKeyboard(e, index)}
>
{tab.label}
</button>
);
})}
</div>
{tabs.map((tab, index) => {
const tabId = `${baseId}-tab-${index}`;
const panelId = `${baseId}-panel-${index}`;
return (
<div
key={index}
role="tabpanel"
id={panelId}
aria-labelledby={tabId}
hidden={activeTab !== index}
>
{tab.content}
</div>
);
})}
</div>
);
}
// Usage
function App() {
return (
<>
<CollapsiblePanel title="More Information">
<p>This content is revealed when the button is clicked.</p>
</CollapsiblePanel>
<Tabs
tabs={[
{ label: 'Overview', content: <p>Overview content</p> },
{ label: 'Features', content: <p>Features content</p> },
{ label: 'Pricing', content: <p>Pricing content</p> },
]}
/>
</>
);
}
