Purpose
An accordion organizes related sections of content, allowing users to expand and collapse individual sections on demand. It is useful when users need to scan headings and selectively reveal detailed content, reducing page scrolling.
Usage Guidance
Use accordions when:
- Content can be grouped into distinct, labeled sections
- Users are likely to need only a subset of sections at once
- Page length would be excessive if all content were visible simultaneously
Avoid accordions when:
- Users typically need all sections open (prefer visible content)
- Sections contain critical information that should always be visible
- There are fewer than two or more than eight sections
Accessibility Considerations
- Each trigger is a
<button>element inside the section heading aria-expandedreflects the open/closed state on the trigger buttonaria-controlslinks the trigger to its content panel- The content panel uses
hiddento hide/show content - No
role="region"is required unless the panel contains a landmark
Keyboard Support
| Key | Action |
|---|---|
Enter / Space |
Toggle the focused accordion panel |
Tab |
Move focus to the next focusable element |
Shift+Tab |
Move focus to the previous focusable element |
Arrow Down |
Move focus to the next accordion trigger |
Arrow Up |
Move focus to the previous accordion trigger |
Home |
Move focus to the first accordion trigger |
End |
Move focus to the last accordion trigger |
Screen Reader Behavior
Screen readers announce the button label, its expanded/collapsed state, and the number of items. When a panel is expanded, screen reader users can navigate into the panel content naturally.
ARIA Requirements
aria-expanded="true|false"on each trigger<button>aria-controls="panel-id"on each trigger linking to its panel- Each panel has a matching
idattribute
Common Mistakes
- Using
<div>instead of<button>for triggers - Omitting
aria-expandedor not updating it on state change - Using CSS
display: nonewithout thehiddenattribute (both are fine, but be consistent) - Placing the trigger outside the heading element (the button should be inside
<h2>,<h3>, etc.) - Animating height without respecting
prefers-reduced-motion
Live Example
A defensible accessibility program is one that can withstand legal scrutiny because it is documented, consistent, and demonstrates good faith effort to meet established standards like WCAG 2.2 AA.
At minimum, conduct a comprehensive audit annually and targeted audits after each major release. High-risk products or those under legal scrutiny may warrant more frequent evaluation.
Cover the most common pairings: NVDA with Firefox, JAWS with Chrome, VoiceOver with Safari on macOS and iOS, and Narrator with Edge. These combinations cover the majority of assistive technology users.
Code Example
HTML
<div class="accordion">
<h3>
<button
class="accordion__trigger"
aria-expanded="false"
aria-controls="panel-1"
>
Section Title
</button>
</h3>
<div id="panel-1" hidden>
<div class="accordion__panel-inner">
<!-- Panel content -->
</div>
</div>
</div>
CSS
.accordion { border: 1px solid #ccd3db; border-radius: 0.5rem; overflow: hidden; }
.accordion h3 { margin: 0; }
.accordion__trigger {
width: 100%; text-align: left; padding: 1rem 1.25rem;
background: #fff; border: none; border-bottom: 1px solid #ccd3db;
font-size: 1rem; font-weight: 600; cursor: pointer;
display: flex; align-items: center; justify-content: space-between;
}
.accordion__trigger:focus-visible {
outline: 3px solid #d4700a;
outline-offset: -3px;
}
.accordion__trigger::after {
content: "";
display: block; width: 0.6em; height: 0.6em;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
transition: transform 0.2s ease;
}
.accordion__trigger[aria-expanded="true"]::after {
transform: rotate(-135deg);
}
@media (prefers-reduced-motion: reduce) {
.accordion__trigger::after { transition: none; }
}
.accordion__panel-inner { padding: 1rem 1.25rem; }
JavaScript
document.querySelectorAll('.accordion').forEach(function(accordion) {
var buttons = accordion.querySelectorAll('.accordion__trigger');
buttons.forEach(function(button) {
button.addEventListener('click', function() {
var expanded = button.getAttribute('aria-expanded') === 'true';
var panel = document.getElementById(button.getAttribute('aria-controls'));
button.setAttribute('aria-expanded', String(!expanded));
panel.hidden = expanded;
});
button.addEventListener('keydown', function(e) {
var idx = Array.from(buttons).indexOf(button);
if (e.key === 'ArrowDown') {
e.preventDefault();
buttons[(idx + 1) % buttons.length].focus();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
buttons[(idx - 1 + buttons.length) % buttons.length].focus();
} else if (e.key === 'Home') {
e.preventDefault();
buttons[0].focus();
} else if (e.key === 'End') {
e.preventDefault();
buttons[buttons.length - 1].focus();
}
});
});
});