Accessibility pattern · Disclosure and content

Accordion

Each section is a heading with a button in it; the button says whether its panel is open. Collapsed panels use hidden="until-found", so find-in-page still reaches the text inside and opens the section.

WCAG criteria
5
Keyboard rules
4
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Orders ship within two working days. Delivery takes three to five days, and you get a tracking link by email.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
Enter or SpaceOpens or closes the section whose heading has focus.
TabMoves to the next heading, or into an open panel's links and fields.
Arrow Down or Arrow UpMoves to the next or previous heading, wrapping at the ends.
Home or EndMoves to the first or last heading.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Focus reaches a closed headingReturns, button, collapsed, heading level 3
Enter opens itExpanded
Tab moves into the open panelReturns, region
Find-in-page matches text in a closed panelThe panel opens and its heading reports expanded

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-accordion" data-ap-accordion>
  <h3 class="ap-accordion__head">
    <button type="button" class="ap-accordion__trigger" id="acc-trigger-1" aria-expanded="true" aria-controls="acc-panel-1">
      <span class="ap-accordion__label">Shipping</span>
      <svg class="ap-accordion__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
    </button>
  </h3>
  <div class="ap-accordion__panel" id="acc-panel-1" role="region" aria-labelledby="acc-trigger-1">
    <p>Orders ship within two working days. Delivery takes three to five days, and you get a tracking link by email.</p>
  </div>

  <h3 class="ap-accordion__head">
    <button type="button" class="ap-accordion__trigger" id="acc-trigger-2" aria-expanded="false" aria-controls="acc-panel-2">
      <span class="ap-accordion__label">Returns</span>
      <svg class="ap-accordion__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
    </button>
  </h3>
  <div class="ap-accordion__panel" id="acc-panel-2" role="region" aria-labelledby="acc-trigger-2" hidden="until-found">
    <p>Return anything unused within 30 days. Print the label from your order page and drop the parcel at any post office.</p>
  </div>

  <h3 class="ap-accordion__head">
    <button type="button" class="ap-accordion__trigger" id="acc-trigger-3" aria-expanded="false" aria-controls="acc-panel-3">
      <span class="ap-accordion__label">Warranty</span>
      <svg class="ap-accordion__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
    </button>
  </h3>
  <div class="ap-accordion__panel" id="acc-panel-3" role="region" aria-labelledby="acc-trigger-3" hidden="until-found">
    <p>Every product carries a two-year warranty against defects. Keep your order email; it is your proof of purchase.</p>
  </div>

  <h3 class="ap-accordion__head">
    <button type="button" class="ap-accordion__trigger" id="acc-trigger-4" aria-expanded="false" aria-controls="acc-panel-4">
      <span class="ap-accordion__label">Payment</span>
      <svg class="ap-accordion__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
    </button>
  </h3>
  <div class="ap-accordion__panel" id="acc-panel-4" role="region" aria-labelledby="acc-trigger-4" hidden="until-found">
    <p>Pay by card, UPI or net banking. Nothing is charged until your order ships.</p>
  </div>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    Each section header is a real heading, so the accordion shows up in a screen reader's list of headings.

  • 2.1.1 Keyboard Level A

    Every section opens and closes with Enter or Space; the arrow keys are a shortcut, not the only way.

  • 2.4.3 Focus Order Level A

    Focus stays on the heading that was used, and Tab reaches an open panel's content straight after it.

  • 2.4.7 Focus Visible Level AA

    Each heading button shows a two-pixel focus ring that clears 3:1 against the surface.

  • 4.1.2 Name, Role, Value Level A

    The button carries aria-expanded and aria-controls, so its name, role and open state are exposed.

Usage

When to use it

Use it

  • Several sections people scan before reading one, like product details or settings groups.
  • Long pages on small screens, where showing every section at once means endless scrolling.

Use something else

  • Content most people need to read in full: hiding it adds a click for everyone.
  • Switching between views of the same thing: tabs say that better.
  • A single show-or-hide section: use the simpler disclosure pattern.

Common failures

How it usually goes wrong

  • A clickable div instead of a button

    A div with a click handler cannot be reached with Tab and says nothing about being expandable. A button inside the heading gives both for free.

  • No open or closed state

    Without aria-expanded a screen reader user cannot tell whether pressing the heading did anything, or which sections are open.

  • Headings replaced by the buttons

    Putting role=button on the heading itself removes it from the headings list, which is how many people move around a page.

  • Collapsed text that cannot be found

    Panels hidden with display: none disappear from find-in-page. hidden="until-found" keeps them searchable and opens the section on a match.

  • Only the chevron is clickable

    A tiny icon target fails people with tremors. The whole heading row is the button here, at least 44 pixels high.

  • Animation with no way to turn it off

    Sliding panels can trigger nausea. The motion here is short and stops when the system asks for reduced motion.

Notes

Building it

  • Use the heading level that fits where the accordion sits in your page; the demo uses h3 because it sits under an h2.
  • role=region on each panel helps when there are a few sections; with more than about six, leave it off, because a page full of regions is noise.
  • hidden="until-found" works in Chromium browsers. Elsewhere the attribute still hides the panel, and the beforematch event simply never fires.
  • A global reset such as [hidden] { display: none !important } also hides until-found panels from find-in-page; the stylesheet restores them for the panel class.
  • The option above switches between one section at a time and several at once; both are valid, so pick the one your content needs.

Sources: WAI-ARIA Authoring Practices: Accordion · HTML: the hidden attribute

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction