Accordion

.accordion is a bordered container for native <details> elements. The open and close behaviour, the keyboard handling and the accessible state are all the element's; Deck restyles the summary row, replaces the disclosure triangle with a chevron, and animates the opening — which until recently was not possible in CSS at all.

When to use it

Use it for a set of sections a reader opens one at a time and mostly leaves closed: frequently asked questions, advanced settings, per-item detail in a long list. Unlike tabs, more than one can be open at once and it needs no JavaScript at all.

No script. Click, or tab to a summary and press Enter.
How is the export generated?
A nightly job reads the project and writes a CSV to your storage bucket.
Can I change the schedule?
Yes — in Settings, under Exports. Times are UTC.
What happens if it fails?
We retry twice, then email the project owner.
<div class="accordion" style="max-inline-size:34rem"> <details open> <summary>How is the export generated?</summary> <div class="accordion-body">A nightly job reads the project and writes a CSV to your storage bucket.</div> </details> <details> <summary>Can I change the schedule?</summary> <div class="accordion-body">Yes — in Settings, under Exports. Times are UTC.</div> </details> <details> <summary>What happens if it fails?</summary> <div class="accordion-body">We retry twice, then email the project owner.</div> </details> </div>
<div class="accordion" style="max-inline-size:34rem">
  <details open>
    <summary>How is the export generated?</summary>
    <div class="accordion-body">A nightly job reads the project and writes a CSV to your storage bucket.</div>
  </details>
  <details>
    <summary>Can I change the schedule?</summary>
    <div class="accordion-body">Yes — in Settings, under Exports. Times are UTC.</div>
  </details>
  <details>
    <summary>What happens if it fails?</summary>
    <div class="accordion-body">We retry twice, then email the project owner.</div>
  </details>
</div>

Animating to auto height

Animating a disclosure open has been a JavaScript problem for twenty years, for one reason: you cannot transition to height: auto, and the height is not known until the content is laid out. Every solution measured the panel with scrollHeight and animated to a pixel value.

Two new pieces remove the need. Deck uses both.

::details-content

A pseudo-element wrapping everything after the <summary>. It can be styled and transitioned, which the content of a <details> previously could not be as a unit.

.accordion details::details-content {
  block-size: 0;
  overflow: hidden;
  transition: block-size var(--dur-3) var(--ease-out),
              content-visibility var(--dur-3) allow-discrete;
}
.accordion details[open]::details-content { block-size: auto; }

interpolate-size: allow-keywords

The transition above animates to block-size: auto, which normally is not interpolable. interpolate-size: allow-keywords — set once on :root in src/02-reset.css, inside an @supports guard — makes the browser interpolate to and from intrinsic size keywords. That single declaration is what makes the rule above work.

content-visibility is in the transition list with allow-discrete for the same reason display is on a modal: it is a discrete property, and deferring its flip to the end keeps the content rendered while the height animates.

Where it is unsupported

A browser that does not know ::details-content ignores the rule, and the element opens and closes instantly — exactly as a <details> always has. Nothing is broken and nothing needs a fallback, which is why Deck ships it unguarded.

The chevron

list-style: none and ::-webkit-details-marker { display: none } remove the platform triangle — both are needed, because the two engines hide it differently. The replacement is a ::after: an 8px square with two borders, rotated 45 degrees, so it reads as a chevron. Open rotates it to 225 degrees.

margin-inline-start: auto pushes it to the end of the summary row, which is the same mechanism as .push and mirrors under dir="rtl" for free.

One at a time

Give every <details> in a group the same name attribute and the browser closes the others when one opens — an exclusive accordion with no script.

Open one and the others close — the name attribute does it
Starter
One project, one collaborator.
Team
Unlimited projects, shared billing, audit log.
Enterprise
SSO, a contract, and a support channel.
<div class="accordion" style="max-inline-size:34rem"> <details name="dx-acc"> <summary>Starter</summary> <div class="accordion-body">One project, one collaborator.</div> </details> <details name="dx-acc"> <summary>Team</summary> <div class="accordion-body">Unlimited projects, shared billing, audit log.</div> </details> <details name="dx-acc"> <summary>Enterprise</summary> <div class="accordion-body">SSO, a contract, and a support channel.</div> </details> </div>
<div class="accordion" style="max-inline-size:34rem">
  <details name="dx-acc">
    <summary>Starter</summary>
    <div class="accordion-body">One project, one collaborator.</div>
  </details>
  <details name="dx-acc">
    <summary>Team</summary>
    <div class="accordion-body">Unlimited projects, shared billing, audit log.</div>
  </details>
  <details name="dx-acc">
    <summary>Enterprise</summary>
    <div class="accordion-body">SSO, a contract, and a support channel.</div>
  </details>
</div>

Exclusive accordions are worth thinking about before using. They stop a reader comparing two sections, and closing something the reader opened is a small surprise every time. Use them when the sections are alternatives, not when they are a list.

A single disclosure

Every rule on this page is written as .accordion …, .disclosure …, so a standalone <details class="disclosure"> gets the same summary row, chevron and animation without the container's border and dividers. Use it for one "show more" in the flow of a page.

.disclosure and .disclosure-body are documented on their own page rather than here — this page's completeness check covers the accordion component only.

Classes

Generated from src/07-components.css by tools/docs/extract.mjs.

Classes, generated from the stylesheet source
Class Layer What it does Source
.accordion deck.components A bordered container for native <details>. The open state, the keyboard and the accessible name are the element's. Deck restyles the summary, replaces the marker with a chevron, and animates the height using ::details-content plus the interpolate-size opt-in in 02-reset.css — the first time a disclosure could animate to auto height without JavaScript. Give the details elements a shared name attribute for one-at-a-time. 07-components.css:431
.accordion-body deck.components No declarations. 07-components.css:460

Tokens

Design tokens this component reads
Token Live Declared value
--surface light-dark(var(--ink-0), var(--ink-950))
--surface-hover light-dark(var(--ink-50), var(--ink-900))
--line light-dark(var(--ink-200), oklch(28% .016 var(--hue-neutral)))
--text-faint light-dark(var(--ink-500), var(--ink-500))
--text-muted light-dark(var(--ink-600), var(--ink-400))
--text-sm .8125rem
--tap 44px
--r-md 12px
--space-3 .75rem
--space-4 1rem
--dur-1 110ms
--dur-2 190ms
--dur-3 300ms

Accessibility

  • The element does everything. <summary> is focusable, operable with Enter and Space, and exposed with an expanded state that the browser keeps in step with the open attribute. There is no ARIA to add and none to get wrong.
  • Put a heading inside the summary, not around it. <summary><h3>…</h3></summary> gives the section a place in the document outline while keeping the summary as the control. A heading wrapping the <details> does not.
  • Content in a closed section is genuinely hidden — not in the accessibility tree, not in the tab order, and not found by the browser's find-in-page in most engines. That is correct behaviour, and it is worth knowing if you expect readers to search the page for something inside a closed panel.
  • Deep links need opening. A link to an id inside a closed <details> will not scroll to it in every browser. Some engines now open the ancestor automatically; do not rely on it.
  • Touch target. The summary is min-block-size: var(--tap) — 44px — and full width, so the whole row is the target.
  • The chevron is decorative and drawn with borders, so it is never announced. The expanded state is carried by the element itself, which is where a screen reader looks for it.
  • Hover has no focus counterpart. .accordion summary:hover changes the background; :focus-visible gets the global focus ring but not the background. The ring is enough, but it is a difference from .menu-item, which styles both.
  • Exclusive accordions move focus nowhere, but they do close a section the reader may have been reading. Consider whether that surprise is worth the tidiness.

Right to left

The summary is a flex row with a gap and logical padding, and the chevron is pushed with margin-inline-start: auto, so it moves to the left edge under dir="rtl" with no extra rules. The chevron's rotation is symmetrical about the vertical axis — it points down when open and toward the block start when closed — so it does not need mirroring either.

The chevron follows the writing direction
كيف يتم إنشاء التصدير؟
مهمة ليلية تقرأ المشروع وتكتب ملف CSV.
هل يمكنني تغيير الجدول الزمني؟
نعم، من الإعدادات.
<div dir="rtl" class="accordion" style="max-inline-size:34rem"> <details open> <summary>كيف يتم إنشاء التصدير؟</summary> <div class="accordion-body">مهمة ليلية تقرأ المشروع وتكتب ملف CSV.</div> </details> <details> <summary>هل يمكنني تغيير الجدول الزمني؟</summary> <div class="accordion-body">نعم، من الإعدادات.</div> </details> </div>
<div dir="rtl" class="accordion" style="max-inline-size:34rem">
  <details open>
    <summary>كيف يتم إنشاء التصدير؟</summary>
    <div class="accordion-body">مهمة ليلية تقرأ المشروع وتكتب ملف CSV.</div>
  </details>
  <details>
    <summary>هل يمكنني تغيير الجدول الزمني؟</summary>
    <div class="accordion-body">نعم، من الإعدادات.</div>
  </details>
</div>

Reduced motion

The height animates over 300ms, the chevron rotates over 190ms, and the summary background transitions over 110ms. The global reset collapses all three to .01ms under prefers-reduced-motion: reduce, which returns the element to its native instant open — the behaviour it had before the animation existed.

This is a good example of an enhancement degrading to the platform default rather than to something broken. Nothing has to be conditionally disabled, because the thing underneath already worked.

Printing

There is no .accordion rule in src/99-print.css, so a printed accordion shows open sections and hides closed ones — the closed content is genuinely not rendered, so it cannot print.

For a page whose accordions hold the actual content — an FAQ, a policy — that means most of the page does not print. One rule fixes it, and it is recorded in FINDINGS.md: @media print { .accordion details { open: true } } is not possible in CSS, but .accordion details::details-content { block-size: auto !important; content-visibility: visible !important } comes close.

Overriding it

@layer app.components {
  /* A chevron that rotates the other way */
  .accordion summary::after { rotate: -45deg; }
  .accordion details[open] summary::after { rotate: 45deg; }

  /* No animation, for a very long list */
  .accordion-instant details::details-content { transition: none; }

  /* A plus/minus instead of a chevron */
  .accordion summary::after { content: "+"; border: 0; rotate: none; }
  .accordion details[open] summary::after { content: "−"; }
}

When not to use it

  • Not for content most readers need. Hiding something behind a click means most people never see it — and it will not print or be found by a page search. An accordion is for the minority case, not the majority one.
  • Not for one of several views. Use tabs, or the exclusive name attribute if you want the accordion shape with one-at-a-time behaviour.
  • Not for a navigation menu. Use .menu or a .drawer. A details element in a nav does not close when the reader clicks elsewhere.
  • Not to make a long page look short. The page is the same length; the reader now has to click eight times to read it. If the content is genuinely long, split it across pages.
  • Not with a form inside a closed section. Fields that are not rendered do not validate and are not focusable, so a required field inside a closed panel produces a submit that fails with nothing visible to fix.