Accessible accordions

Live demo

Why neurodivergent users benefit from accordions

Long pages with all content visible at once are a significant barrier for ADHD and autistic users. Accordions reduce cognitive load by hiding non-relevant content until the user requests it, letting people focus on one section at a time without the distraction of surrounding text.

For screen reader users, a well-built accordion announces its expanded or collapsed state on every interaction — no guessing whether clicking did anything. The key is using a real <button> element so keyboard and assistive technology support comes for free.

Required WCAG success criteria

  • SC 4.1.2 Name, Role, Value (A) — the toggle button must expose its name, its role (button), and its current state (expanded or collapsed) to assistive technology via aria-expanded.
  • SC 2.1.1 Keyboard (A) — every accordion header must be reachable and operable by keyboard alone. Enter and Space must toggle the panel; Tab must move to the next interactive element.
  • SC 2.4.3 Focus Order (A) — when a panel opens, the focus must remain on the trigger button. Moving focus into the panel on open is not required and often disorienting.
  • SC 2.4.7 Focus Visible (AA) — the focused accordion header button must have a clearly visible focus indicator (minimum 3:1 contrast ratio for the focus ring, per SC 2.4.11 AAA).

The correct ARIA pattern

The ARIA Authoring Practices Guide (APG) defines the accordion pattern. The two essential associations are:

  • The button carries aria-expanded (true when open, false when closed) and aria-controls pointing to the panel's id.
  • The panel carries role="region" (only recommended when there are fewer than six panels — too many landmarks is noisy) and aria-labelledby pointing back to the button's id.
<!-- Accordion item — closed state -->
<div class="accordion-item">
  <h3>
    <button
      id="acc-btn-1"
      aria-expanded="false"
      aria-controls="acc-panel-1"
      class="accordion-trigger"
    >
      What documents do I need to apply?
    </button>
  </h3>
  <div
    id="acc-panel-1"
    role="region"
    aria-labelledby="acc-btn-1"
    hidden
  >
    <div class="accordion-panel-inner">
      <p>You will need a government-issued photo ID and proof of address.</p>
    </div>
  </div>
</div>

<!-- Accordion item — open state -->
<div class="accordion-item">
  <h3>
    <button
      id="acc-btn-2"
      aria-expanded="true"
      aria-controls="acc-panel-2"
      class="accordion-trigger"
    >
      How long does the review process take?
    </button>
  </h3>
  <div
    id="acc-panel-2"
    role="region"
    aria-labelledby="acc-btn-2"
  >
    <div class="accordion-panel-inner">
      <p>Reviews are completed within 10 business days.</p>
    </div>
  </div>
</div>

Keyboard interaction

  • Enter or Space — toggles the focused accordion panel open or closed.
  • Tab — moves focus to the next accordion header (or the next focusable element after all accordions).
  • Shift + Tab — moves focus to the previous accordion header or the element before the accordion group.
  • Arrow keys (optional) — the APG also defines Up/Down arrows to move between headers without Tab. This is optional for accordions (unlike tab panels) but add it if your users expect it.

Minimal JavaScript toggle

document.querySelectorAll('.accordion-trigger').forEach((btn) => {
  btn.addEventListener('click', () => {
    const expanded = btn.getAttribute('aria-expanded') === 'true';
    const panelId = btn.getAttribute('aria-controls');
    const panel = document.getElementById(panelId);

    btn.setAttribute('aria-expanded', String(!expanded));

    if (expanded) {
      panel.setAttribute('hidden', '');
    } else {
      panel.removeAttribute('hidden');
    }
  });
});

Using the hidden attribute instead of display: none via a CSS class means the browser natively hides the panel from both visual and assistive technology rendering — no extra aria-hidden needed.

Common mistakes

  • Using a <div> as the trigger — a div has no implicit role, no keyboard activation, and no aria-expanded support. Screen reader users and keyboard users cannot activate it. Always use <button>.
  • CSS display: none without updating ARIA — hiding with CSS alone does not update aria-expanded. You must set both the visual state and the ARIA attribute together.
  • No visible focus indicator — removing the default outline without providing a replacement fails SC 2.4.7. Use a custom outline that meets 3:1 contrast.
  • Wrapping the entire header in an anchor tag — anchor elements navigate; they should not be used for toggle actions. The trigger must be a <button> inside the heading, not an <a>.
  • Forgetting heading structure — each accordion trigger should be wrapped in an appropriate heading element (e.g., <h3>) so the section structure is navigable via screen reader heading shortcuts.

Testing checklist

  • Tab to each accordion header — confirm focus is visible and button role is announced.
  • Press Enter and Space — confirm the panel opens and aria-expanded="true" is announced.
  • With a screen reader (NVDA + Firefox, VoiceOver + Safari) — navigate to a closed header and activate it; confirm the screen reader announces "expanded" or "collapsed" on state change.
  • Inspect the DOM — confirm hidden panels have hidden attribute or equivalent, and that aria-expanded matches visual state.
  • Run axe-core — check for aria-required-children and button-name violations.