Skip to Content

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:

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 id referenced by aria-controls
  • Wrap the button in a heading element (<h2>, <h3>, etc.) for document structure
  • The panel should use the hidden attribute (not just CSS display: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?
Free shipping is available on orders over $50.
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.

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-expanded means the open/closed state is invisible to assistive technology

What the screen reader announces:

VersionAnnouncement 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”

Resources