DisclosureFlagship guide
Accordion
A vertically stacked set of headers that toggle the visibility of associated content panels.
Last verified against WCAG 2.2 and WAI-ARIA APG 1.2 on 2026-07-02.
Implementation
Orders ship within 2 business days via standard carrier.
Items can be returned within 30 days of delivery for a full refund.
All products include a 1-year limited manufacturer warranty.
accordion-pattern.tsx
function AccordionPattern() {
const [openIds, setOpenIds] = useState(new Set(["shipping"]));
const headerRefs = useRef({});
function toggle(id) {
setOpenIds(prev => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
}
function onKeyDown(e, index) {
let nextIndex = null;
if (e.key === "ArrowDown") nextIndex = (index + 1) % SECTIONS.length;
if (e.key === "ArrowUp") nextIndex = (index - 1 + SECTIONS.length) % SECTIONS.length;
if (e.key === "Home") nextIndex = 0;
if (e.key === "End") nextIndex = SECTIONS.length - 1;
if (nextIndex !== null) {
e.preventDefault();
headerRefs.current[SECTIONS[nextIndex].id]?.focus();
}
}
return SECTIONS.map((section, i) => {
const expanded = openIds.has(section.id);
return (
<h3 key={section.id}>
<button
ref={el => (headerRefs.current[section.id] = el)}
aria-expanded={expanded}
aria-controls={`panel-${section.id}`}
id={`header-${section.id}`}
onClick={() => toggle(section.id)}
onKeyDown={e => onKeyDown(e, i)}
>
{section.title}
</button>
<div
id={`panel-${section.id}`}
role="region"
aria-labelledby={`header-${section.id}`}
hidden={!expanded}
>
{section.body}
</div>
</h3>
);
});
}Live demo
Orders ship within 2 business days via standard carrier.
Items can be returned within 30 days of delivery for a full refund.
All products include a 1-year limited manufacturer warranty.
Required roles, states & properties
| Element | Attribute | Why |
|---|---|---|
| Header <button> | aria-expanded | Communicates whether this section's panel is currently visible — announced as "expanded" or "collapsed." |
| Header <button> | aria-controls | Associates the header with the panel id it toggles, so AT can relate the two even though they aren't nested. |
| Panel | role="region" + aria-labelledby | Lets a screen reader user jump directly to an open panel via landmark/region navigation, labeled by its header. |
| Header wrapper | Native heading element (h3) | Keeps the accordion navigable via a screen reader's heading list, independent of the ARIA toggle semantics. |
Keyboard interaction model
| Key | Behavior |
|---|---|
| Tab / Shift+Tab | Moves focus into and out of the accordion, stopping only on header buttons (collapsed panels are not in the tab order). |
| Enter / Space | Toggles the focused header's panel open or closed. |
| Down Arrow | Moves focus to the next header. |
| Up Arrow | Moves focus to the previous header. |
| Home / End | Moves focus to the first / last header. |
Focus management rules
- Only header buttons are ever in the Tab order — panel content is never reachable while its panel is collapsed.
- Toggling a panel never moves focus away from its header.
- Arrow Up/Down/Home/End move focus directly between headers, independent of open/closed state.
WCAG 2.2 success criteria mapping
| SC | Name | Level | Why it applies |
|---|---|---|---|
| 1.3.1 | Info and Relationships | A | Structure and relationships conveyed visually are also programmatically determinable. |
| 2.1.1 | Keyboard | A | All functionality is operable through a keyboard interface with no specific timing. |
| 2.4.6 | Headings and Labels | AA | Headings and labels describe topic or purpose. |
| 2.4.7 | Focus Visible | AA | Any keyboard-operable UI has a visible focus indicator. |
| 4.1.2 | Name, Role, Value | A | For all UI components, name, role, and value are programmatically determinable; states and changes are announced. |