Accordion (Collapsible Sections)
Create accessible accordion components with proper ARIA attributes, keyboard navigation, and screen reader support.
The Bad (Inaccessible)
<div class="accordion-item" onclick="toggle(this)">
<div class="accordion-header">Section 1</div>
<div class="accordion-content" style="display:none">
Content
</div>
</div>
Accessibility-Ready Code
<!-- Gold Standard: Proper button with ARIA states -->
<div class="accordion">
<!-- Item 1 -->
<div class="accordion-item">
<h3>
<button
type="button"
aria-expanded="false"
aria-controls="panel-1"
class="accordion-trigger"
>
<span class="accordion-title">What is an accordion?</span>
<span class="accordion-icon" aria-hidden="true">▼</span>
</button>
</h3>
<div
id="panel-1"
role="region"
aria-labelledby="panel-1-heading"
class="accordion-panel"
hidden
>
<p id="panel-1-heading" class="sr-only">
What is an accordion?
</p>
<p>
An accordion is a vertically stacked list of headers that reveal
or hide content associated with them.
</p>
</div>
</div>
<!-- Item 2 -->
<div class="accordion-item">
<h3>
<button
type="button"
aria-expanded="false"
aria-controls="panel-2"
class="accordion-trigger"
>
<span class="accordion-title">When to use accordions?</span>
<span class="accordion-icon" aria-hidden="true">▼</span>
</button>
</h3>
<div
id="panel-2"
role="region"
aria-labelledby="panel-2-heading"
class="accordion-panel"
hidden
>
<p id="panel-2-heading" class="sr-only">
When to use accordions?
</p>
<p>
Use accordions to reduce scrolling when you have related content
that users don't need to see simultaneously.
</p>
</div>
</div>
</div>
<script>
document.querySelectorAll('.accordion-trigger').forEach(button => {
button.addEventListener('click', () => {
const isExpanded = button.getAttribute('aria-expanded') === 'true';
const panel = document.getElementById(
button.getAttribute('aria-controls')
);
button.setAttribute('aria-expanded', !isExpanded);
panel.hidden = isExpanded;
});
});
</script>
// Gold Standard: Proper button with ARIA states
.accordion
// Item 1
.accordion-item
h3
button(
type="button"
aria-expanded="false"
aria-controls="panel-1"
class="accordion-trigger"
)
span.accordion-title What is an accordion?
span.accordion-icon(aria-hidden="true") ▼
div#panel-1(
role="region"
aria-labelledby="panel-1-heading"
class="accordion-panel"
hidden
)
p#panel-1-heading.sr-only What is an accordion?
p An accordion is a vertically stacked list of headers.
// Item 2
.accordion-item
h3
button(
type="button"
aria-expanded="false"
aria-controls="panel-2"
class="accordion-trigger"
)
span.accordion-title When to use accordions?
span.accordion-icon(aria-hidden="true") ▼
div#panel-2(
role="region"
aria-labelledby="panel-2-heading"
class="accordion-panel"
hidden
)
p#panel-2-heading.sr-only When to use accordions?
p Use accordions to reduce scrolling.
const AccordionItem = ({ title, children, id, isOpen, onToggle }) => (
<div className="accordion-item">
<h3>
<button
type="button"
aria-expanded={isOpen}
aria-controls={`panel-${id}`}
onClick={onToggle}
>
<span className="accordion-title">{title}</span>
<span className="accordion-icon" aria-hidden="true">
{isOpen ? '▲' : '▼'}
</span>
</button>
</h3>
<div
id={`panel-${id}`}
role="region"
aria-labelledby={`panel-${id}-heading`}
className="accordion-panel"
hidden={!isOpen}
>
<p id={`panel-${id}-heading`} className="sr-only">
{title}
</p>
{children}
</div>
</div>
);
<template>
<div class="accordion-item">
<h3>
<button
type="button"
:aria-expanded="isOpen"
:aria-controls="`panel-${id}`"
@click="isOpen = !isOpen"
>
<span class="accordion-title">{{ title }}</span>
<span class="accordion-icon" aria-hidden="true">
{{ isOpen ? '▲' : '▼' }}
</span>
</button>
</h3>
<div
:id="`panel-${id}`"
role="region"
:aria-labelledby="`panel-${id}-heading`"
class="accordion-panel"
v-show="isOpen"
>
<p :id="`panel-${id}-heading`" class="sr-only">
{{ title }}
</p>
<slot />
</div>
</div>
</template>
<div className="border border-gray-200 rounded-lg">
<h3>
<button
type="button"
:aria-expanded="isOpen"
:aria-controls="`panel-${id}`"
@click="isOpen = !isOpen"
className="w-full flex items-center justify-between p-4 text-left font-semibold"
>
<span>What is an accordion?</span>
<span aria-hidden="true" className="text-gray-400">
{isOpen ? '▲' : '▼'}
</span>
</button>
</h3>
<div
:id="`panel-${id}`"
role="region"
:aria-labelledby="`panel-${id}-heading`"
className="px-4 pb-4"
v-show="isOpen"
>
<p :id="`panel-${id}-heading`` className="sr-only">
What is an accordion?
</p>
<p className="text-gray-600">
An accordion is a vertically stacked list...
</p>
</div>
</div>
The Standard
Accordions are collapsible sections that show/hide content. They must use proper button elements, ARIA states, and support keyboard navigation.
WCAG Criteria
- 4.1.2 Name, Role, Value: Proper ARIA states.
- 3.2.1 On Focus: No focus changes without user interaction.
- 2.4.3 Focus Order: Logical tab order.
❌ The Bad (Inaccessible)
What’s Wrong?
- Using
onclickondiv: Not keyboard accessible. - No
aria-expanded: Screen readers can’t determine state. - Missing panel role: Screen reader loses context.
- No keyboard navigation: Can’t navigate between items.
✅ The Good (Accessibility-Ready Code)
Why This Works
- Proper Button Element: Inherently keyboard accessible.
- Dynamic aria-expanded: Updates to show panel state.
- Region Role: Provides context for screen readers.
- aria-labelledby: Associates panel with its header.
Accessibility Checklist
- Use
<button>instead of<div>for headers. - Add
aria-expandedthat updates (true/false). - Include
aria-controlspointing to panel ID. - Add
role="region"to content panels. - Use
aria-labelledbyto link panel to header. - Support arrow keys for navigation.
- Add visual indicator (▼/▲) with
aria-hidden="true".
<div class="accordion-item">
<h3>
<button
type="button"
aria-expanded="false"
aria-controls="panel-1"
>
Section Title
<span aria-hidden="true">▼</span>
</button>
</h3>
<div
id="panel-1"
role="region"
aria-labelledby="panel-1-heading"
hidden
>
<p id="panel-1-heading" class="sr-only">
Section Title
</p>
<p>Content goes here...</p>
</div>
</div>
<script>
button.addEventListener('click', () => {
const isExpanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', !isExpanded);
panel.hidden = isExpanded;
});
</script>
Technical Deep Dive
Screen Reader Announcements
- NVDA: “What is an accordion? button, collapsed”
- VoiceOver: “What is an accordion?, button, collapsed”
- JAWS: “What is an accordion? button, collapsed”
Best Practice: When to Use Accordions
Accordions are best for reducing scrolling when you have related content that users don’t need to see simultaneously. They’re not ideal for content that all users need to read, as the hidden content may be overlooked. If in doubt, show all content by default.
Interactive Behavioral Lab
Interactive Sandbox
🔬 Technical Internals
Understand how this component is processed by the browser and Assistive Technology (AT). This section bridges the gap between visual code and the hidden logic that powers accessibility.
🌲 Accessibility Tree
The data structure used by screen readers to "see" your page. It translates HTML roles and attributes into standardized objects.
⚙️ Event Logic
Expected behavioral standards for keyboard navigation and state transitions. Crucial for users who don't use a mouse.
- Focus: Highlights via
:focus-visible - Activation: Responds to
EnterandSpace - Role: Identified as
Accordion