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.
How is the export generated?
Can I change the schedule?
What happens if it fails?
<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.
Starter
Team
Enterprise
<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.
| 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
| 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 theopenattribute. 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
idinside 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:hoverchanges the background;:focus-visiblegets 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.
كيف يتم إنشاء التصدير؟
هل يمكنني تغيير الجدول الزمني؟
<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
nameattribute if you want the accordion shape with one-at-a-time behaviour. -
Not for a navigation menu. Use
.menuor 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.