Patterns Accordion

Accordion

Vertically stacked headers that expand and collapse the content beneath them. The native <details> element is the answer for almost every case — keyboard, ARIA, and state announced for free.

Reviewed against the methodology checklist Updated

Try the keyboard

When to use

Use an accordion when a set of related sections each have content the user might want to read but probably doesn’t. FAQs, settings groups, “what’s included” tables on pricing pages.

Don’t use an accordion when:

The pattern

Accessible accordion (native <details>)
When is the next issue?

Sundays at 7am UTC. We have never missed one. (Lena once nearly did, in week 14, snowed in.)

Can I gift a subscription?

Yes. Email us with the recipient and we set it up — no awkward gift card, no email to them until they want it.

Do you have an RSS feed?

Members get a private RSS feed with the full text of every issue. Public preview feed is on the way.

In 2026, the answer is <details> and <summary>. The browser:

HTML
<details>
<summary>When is the next issue?</summary>
<p>Sundays at 7am UTC. We have never missed one.</p>
</details>

That’s a complete, accessible accordion item. Stack several of them and you have an accordion group.

Letting only one open at a time

If you need exclusive accordion behaviour (open one, close the others), give every <details> element the same name attribute. It’s an HTML feature, no JS:

HTML
<details name="faq"><summary>One</summary>…</details>
<details name="faq"><summary>Two</summary>…</details>
<details name="faq"><summary>Three</summary>…</details>

Activating one closes the others — like a radio group, but for disclosures.

Styling the marker

Browsers render a default disclosure triangle inside <summary>. Replace it with whatever you like:

CSS
summary {
list-style: none; /* Firefox + Chromium */
}
summary::-webkit-details-marker {
display: none; /* old Safari */
}

/* now add your own indicator */
summary::after {
content: '+';
margin-inline-start: auto;
transition: transform 150ms;
}
details[open] summary::after {
transform: rotate(45deg); /* + becomes × */
}

Don’t remove the indicator entirely — sighted users need a visual cue that the summary is interactive.

Anti-pattern: rolling your own with divs

The classic version, found in countless component libraries:

HTML
<div class="accordion">
<div class="accordion__header" onclick="toggle(this)">
  When is the next issue?
</div>
<div class="accordion__panel hidden">
  Sundays at 7am UTC.
</div>
</div>

The complete list of failures:

  1. Header isn’t focusable (no tabindex) — keyboard users can’t reach it.
  2. No keyboard handler — even with tabindex, Enter/Space don’t toggle (need keydown handler).
  3. No role announced — screen readers say “When is the next issue” with no indication that it’s interactive.
  4. No state announced — opening it changes nothing audible. The user can’t tell if their action did anything.
  5. No relationship between header and panelaria-controls would name the panel; aria-expanded would announce the state. Neither is here.

To make it actually work you’d need: tabindex="0", role="button", aria-expanded="true|false", aria-controls="panel-id", a keydown handler for Enter/Space, plus the click handler. That’s a lot of code to reinvent <details>.

If you absolutely cannot use <details> (e.g. you need the panel to render outside the summary’s DOM), use <button> for the header — at least you’ll get role and keyboard for free:

HTML
<h3>
<button type="button" aria-expanded="false" aria-controls="faq-1">
  When is the next issue?
</button>
</h3>
<div id="faq-1" hidden>
Sundays at 7am UTC.
</div>

Common variations

Checklist

Screen reader transcript

Voiced by George — reading what VoiceOver on macOS Safari announces. Other readers (NVDA, JAWS) phrase things differently; the meaning is what matters.

Screen reader announcements for each step
ListenYou doScreen reader says
Tab onto a closed summaryWhen is the next issue, collapsed, button.
Press EnterExpanded.
Tab into the now-revealed body(next focusable inside the body, if any)

Screen reader transcript — anti-pattern

Same interaction, but on the broken version above. Notice what the user is missing.

Screen reader announcements for each step
ListenYou doScreen reader says
Tab onto a div-as-summary(focus skips it entirely — div isn't focusable)
Reach it after adding tabindexWhen is the next issue. (no role, no state — sounds like static text)

WCAG references