Overview
Accordions (disclosure widgets) show and hide sections of content. The most common mistake is using a <div> or <span> as the trigger instead of a <button>. This breaks keyboard access entirely — users who navigate by keyboard or switch device can never open the panels.
WCAG Criteria:
- 4.1.2 Name, Role, Value — the trigger must expose its role (
button) and expanded state (aria-expanded)
Key requirements:
- The trigger must be a
<button>(not a<div>) - Use
aria-expanded="true/false"to communicate open/closed state - The panel must have a stable
idreferenced byaria-controls - Wrap the button in a heading element (
<h2>,<h3>, etc.) for document structure - The panel should use the
hiddenattribute (not just CSSdisplay:none) so it is fully removed from the accessibility tree when closed
Accordion Panel
Button-Driven Accordion vs. Div Header
Inaccessible
<!-- div is not focusable and has no button role -->
<div class="accordion-header" onclick="toggle(this)">
What is your return policy?
</div>
<div class="accordion-panel">
We accept returns within 30 days of purchase with a valid receipt.
</div>
<div class="accordion-header" onclick="toggle(this)">
Do you offer free shipping?
</div>
<div class="accordion-panel" style="display:none">
Free shipping is available on orders over $50.
</div>Live Preview
What is your return policy?
We accept returns within 30 days of purchase with a valid receipt.
Do you offer free shipping?
Accessible
<h3 style="margin:0">
<button
id="acc-btn-1"
aria-expanded="true"
aria-controls="acc-panel-1"
style="width:100%;text-align:left;"
onclick="
var expanded = this.getAttribute('aria-expanded') === 'true';
this.setAttribute('aria-expanded', String(!expanded));
var panel = document.getElementById(this.getAttribute('aria-controls'));
if (!expanded) { panel.removeAttribute('hidden'); } else { panel.setAttribute('hidden', ''); }
"
>
What is your return policy?
</button>
</h3>
<div id="acc-panel-1" role="region" aria-labelledby="acc-btn-1">
We accept returns within 30 days of purchase with a valid receipt.
</div>
<h3 style="margin:0">
<button
id="acc-btn-2"
aria-expanded="false"
aria-controls="acc-panel-2"
style="width:100%;text-align:left;"
onclick="
var expanded = this.getAttribute('aria-expanded') === 'true';
this.setAttribute('aria-expanded', String(!expanded));
var panel = document.getElementById(this.getAttribute('aria-controls'));
if (!expanded) { panel.removeAttribute('hidden'); } else { panel.setAttribute('hidden', ''); }
"
>
Do you offer free shipping?
</button>
</h3>
<div id="acc-panel-2" role="region" aria-labelledby="acc-btn-2" hidden>
Free shipping is available on orders over $50.
</div>Live Preview
We accept returns within 30 days of purchase with a valid receipt.
Free shipping is available on orders over $50.
What’s wrong with the div accordion?
- A
<div>has no button role — screen readers don’t identify it as interactive - It cannot receive keyboard focus with Tab
- It cannot be activated with Enter or Space
- No
aria-expandedmeans the open/closed state is invisible to assistive technology
What the screen reader announces:
| Version | Announcement on focus |
|---|---|
Inaccessible (<div>) | “What is your return policy?” — no role, no state |
Accessible (<button>) | “What is your return policy?, collapsed, button” |
| When expanded | ”What is your return policy?, expanded, button” |