An accordion section that shows or hides content via CSS display: none / block toggling — but has no aria-expanded on the trigger button — is invisible in state to screen readers. The screen reader announces 'button: FAQ: What is your refund policy?' whether the section is open or closed. Users who want to collapse an open section have no way to confirm its current state before clicking. WCAG 2.2 SC 4.1.2 (Name, Role, Value) requires the state of UI components to be programmatically determinable. Section 508 2018 Refresh 502.3.4 covers programmatic state. The ARIA disclosure widget pattern specifies aria-expanded on the controlling button and id/aria-controls linking the button to its panel.
Medium because absent `aria-expanded` removes state awareness from screen reader users, degrading their ability to navigate and manage accordions and disclosure widgets.
Add aria-expanded to the trigger button and link it to the controlled panel with aria-controls. Ensure the panel is hidden in a way that assistive technology respects — use hidden attribute or display: none, not visibility: hidden or opacity: 0.
const Accordion = ({ items }: { items: { id: string; title: string; body: string }[] }) => {
const [openId, setOpenId] = useState<string | null>(null);
return (
<div className="accordion">
{items.map(item => (
<div key={item.id} className="accordion-item">
<h3>
<button
aria-expanded={openId === item.id}
aria-controls={`panel-${item.id}`}
id={`btn-${item.id}`}
onClick={() => setOpenId(id => id === item.id ? null : item.id)}
>
{item.title}
</button>
</h3>
<div
id={`panel-${item.id}`}
role="region"
aria-labelledby={`btn-${item.id}`}
hidden={openId !== item.id}
>
<p>{item.body}</p>
</div>
</div>
))}
</div>
);
};
Using hidden (not CSS display: none alone) ensures assistive technology does not read the panel content when collapsed. If using CSS for animation, remove hidden but add aria-hidden={openId !== item.id} to maintain the programmatic state signal.
ID: accessibility-basics.forms-interactive.expandable-components
Severity: medium
What to look for: Enumerate every relevant item. Check collapsible/expandable components (accordions, disclosure widgets, expand/collapse sections). These should use aria-expanded="true" or aria-expanded="false" to communicate whether the content is visible.
Pass criteria: At least 1 of the following conditions is met. Expandable components have aria-expanded attribute. The attribute correctly reflects the visibility state.
Fail criteria: Expandable components lack aria-expanded, or the attribute does not match the visible state.
Skip (N/A) when: The application has no expandable components.
Detail on fail: Identify expandable components without aria-expanded. Example: "Accordion panels use CSS to hide/show content but have no aria-expanded. Users cannot tell if sections are expanded or collapsed."
Remediation: Add aria-expanded to expandable components:
const AccordionItem = ({ title, content, isExpanded, onToggle }) => (
<div className="accordion-item">
<button
aria-expanded={isExpanded}
aria-controls="accordion-content"
onClick={onToggle}
>
{title}
</button>
{isExpanded && (
<div id="accordion-content" className="accordion-content">
{content}
</div>
)}
</div>
);