Accordion
A native <details> styled via a single class. Animated open and close via ::details-content + the CSS Grid 0fr β 1fr trick + transition-behavior: allow-discrete. Single-open grouping via the native name attribute. Two variants, three sizes, color theming. No JavaScript.
Install
import "elements-kit/ui/styles/theme.css";import "elements-kit/ui/styles/scaling.css";import "elements-kit/ui/styles/radius.css";import "elements-kit/ui/styles/space.css";import "elements-kit/ui/styles/typography.css";import "elements-kit/ui/styles/cursor.css";import "elements-kit/ui/styles/unset.css";// pick a neutral palette for --neutral-* (must match data-neutral on root):import "elements-kit/ui/styles/palette/gray.css";import "elements-kit/ui/styles/neutral/gray.css";// and the accordion itself:import "elements-kit/ui/accordion/accordion.css";API
<!-- Single-open group: same `name` on all items --><details class="x-accordion" name="faq" data-size="2"> <summary>How does it work?</summary> <p>Any HTML inside.</p></details><details class="x-accordion" name="faq"> <summary>Is it accessible?</summary> <p>Native disclosure semantics.</p></details>
<!-- Multi-open: omit `name` --><details class="x-accordion"> <summary>Standalone</summary> <p>Content.</p></details>| Attribute | Values |
|---|---|
name (native HTML) | any string β items sharing a name form an exclusive group |
data-variant | surface (default β bordered card), soft (tinted block, no border), borderless (no chrome β color-only hover) |
data-size | 1, 2 (default), 3 |
data-accent (on parent or self) | any imported color scale |
aria-disabled="true" | greys out and stops clicks; screen readers announce as disabled |
open (native HTML) | initial open state |
Sizing
data-size | summary padding | font-size |
|---|---|---|
1 | --space-2 / --space-3 | --font-size-2 |
2 (default) | --space-3 / --space-4 | --font-size-3 |
3 | --space-4 / --space-5 | --font-size-4 |
Variants
surface(default) β--color-materialbackground with a soft--neutral-a5border and--radius-4corners. In light mode the body sits at--neutral-a2; in dark mode that tint moves to the summary. Stacked siblings collapse their adjoining borders into one card.softβ surface minus the border. Shares the panel bg, per-mode alpha flip on summary/body, and hover behavior; just no1px solidring around it. Mid-emphasis betweensurfaceandborderless.borderlessβ no border, no background, no horizontal padding. Hover affordance is a text-color shift (a12 β 12) β no bg wash. The trigger color also stays at full strength while[open]. For FAQs inside body copy.
The primitive ships no chevron β keep it quiet by default. If you want one, place an SVG (or anything else) inside <summary> and rotate it with your own [open] > summary svg { transform: rotate(180deg); } rule. See the playground for an example.
Single-open grouping
Browser-enforced via the native name attribute. Opening one item closes its siblings.
<details class="x-accordion" name="faq"><summary>One</summary>β¦</details><details class="x-accordion" name="faq"><summary>Two</summary>β¦</details><details class="x-accordion" name="faq"><summary>Three</summary>β¦</details>Omit name and each item opens independently.
Animation
Three pieces cooperate to animate both open and close, cross-browser:
::details-contentβ addresses the content wrapper as a pseudo-element so we can style it without an extra DOM node.display: grid; grid-template-rows: 0fr β 1frβ the classic grid trick. The implicit row track holds the rendered children of<details>(minus<summary>). Transitioning the track between0frand1fris universal CSS β nointerpolate-sizeneeded, so it works in every current browser.padding-block-endis animated alongside so the bottom padding grows in lockstep.transition-behavior: allow-discreteoncontent-visibilityβ defers the browserβs instant content-hide until the row collapse finishes, so the close animation runs to completion.
Without allow-discrete, the browser would yank the content the moment [open] flipped off and the close would be invisible.
Respects prefers-reduced-motion: reduce β transitions are removed.
States
[open]β content expanded.:focus-visibleonsummaryβ 2px--focus-8outline at-2pxinset offset.aria-disabled="true"β0.6opacity,pointer-events: noneon the summary.<details>has no nativedisabledand isnβt a form control, so:disableddoesnβt apply;aria-disabledis the spec-correct hook and is announced by screen readers.
Nested accordions
Nest a <details class="x-accordion"> inside anotherβs content. Cascade Just Works β sizing tokens scope per item, single-open grouping scopes per name.
<details class="x-accordion" name="outer"> <summary>Parent</summary> <details class="x-accordion" name="inner"> <summary>Child</summary> <p>Deep content.</p> </details></details>Theming
Set data-accent="<color>" on the accordion or any ancestor to theme the accent. Light/dark flips automatically via the .dark class on a parent β see Light & dark.
Accessibility
<details> / <summary> is a native disclosure widget. Keyboard support (Enter, Space to toggle) and screen-reader announcement (disclosure triangle, collapsed/expanded) come from the browser.
- Put the trigger label in
<summary>directly β wrap with<h3>inside the summary only if document outline matters. Donβt wrap<details>in a heading. - For long content panels, the disclosure region is implicitly labeled by the summary. No
aria-controlsneeded.
Browser support
| Feature | Chrome | Safari | Firefox |
|---|---|---|---|
<details name> grouping | 120+ | 17.4+ | 129+ |
::details-content | 131+ | 18.2+ | 137+ |
transition-behavior: allow-discrete | 117+ | 17.4+ | 129+ |
grid-template-rows transition | universal | universal | universal |
All current browsers support the full set. Older browsers degrade to instant open and close β disclosure still works; grouping falls back to multi-open. No JavaScript fallback is shipped.