Component
accordion
Demo
tokens.css,
base.css and accordion.css. No scripts.
Source
Copy both files, or run npx nojsui add accordion.
Browser support
Shipped in every major engine, but only recently — older versions need the fallback.
| Feature | Baseline | Chrome | Edge | Firefox | Safari | Chrome Android | Firefox Android | Safari iOS |
|---|---|---|---|---|---|---|---|---|
| <details> | widely | 12 | 79 | 49 | 6 | 18 | 49 | 6 |
| Mutually exclusive <details> elements | newly | 120 | 120 | 130 | 17.2 | 120 | 130 | 17.2 |
| Feature | Baseline | Chrome | Edge | Firefox | Safari | Chrome Android | Firefox Android | Safari iOS |
|---|---|---|---|---|---|---|---|---|
| ::details-content | newly | 131 | 131 | 143 | 18.4 | 131 | 143 | 18.4 |
| interpolate-size | limited | 129 | 129 | — | — | 129 | — | — |
Usage
<div class="sk-accordion" data-exclusive>
<details class="sk-accordion__item" name="faq" open>
<summary class="sk-accordion__summary">Why zero JavaScript?</summary>
<div class="sk-accordion__panel">
<p class="sk-accordion__text">…</p>
</div>
</details>
<details class="sk-accordion__item" name="faq">…</details>
</div>
Two variants, one attribute. Give every <details> the same name and the
group is exclusive — opening one closes the others, enforced by the browser.
Remove the name and the panels open independently. Nothing else changes.
data-exclusive on the wrapper is documentation for whoever reads the markup;
it has no behaviour attached. The name is what does the work.
How it works
There is no accordion here, only <details>. Open and closed state, the
toggle, Enter/Space, the disclosure semantics and the accessibility tree are
all the element’s own. This component contributes a marker, a panel and — where
the engine can — a height animation.
Verified in Chromium, WebKit and Gecko: exclusive grouping closes the sibling,
Enter on a <summary> toggles, and a closed panel’s content is absent from
the accessibility tree rather than merely invisible.
The chevron is drawn with borders. Two edges of a square, rotated — no icon
font, no SVG, nothing to load, and it inherits currentColor so it follows the
theme for free. The UA triangle is removed with list-style: none plus a
::marker reset, verified gone in all three engines.
The height animation is the only enhanced part
Animating to auto requires interpolate-size: allow-keywords, which is
Chromium-only today. So the whole animation block sits inside @supports, and
engines without it open and close instantly.
That fallback is not a degraded experience so much as a plainer one: the panel
content is never hidden, delayed or clipped — it simply appears. Nothing outside
the @supports block depends on anything inside it.
The animation also sits inside a prefers-reduced-motion: no-preference guard,
so a reduced-motion user gets the same instant open as an engine without
interpolate-size.
Theming
Set any of these anywhere above the component — :root, a section wrapper, or
one instance. accordion.css only ever reads them, so the nearest declaration wins
(ADR 0011).
| Property | Default | Controls |
|---|---|---|
--sk-accordion-bg | --sk-color-surface | Accordion background |
--sk-accordion-border-color | --sk-color-border | Outer border and the rule between items |
--sk-accordion-radius | --sk-radius-lg | Corner radius |
--sk-accordion-summary-fg | --sk-color-text | Summary text |
--sk-accordion-summary-bg-hover | --sk-color-surface-raised | Summary background on hover |
--sk-accordion-marker-color | --sk-color-text-muted | The chevron |
--sk-accordion-text-fg | --sk-color-text-muted | Panel prose |
:root { --sk-accordion-radius: 0; }
Whether opening one item closes the others is the name attribute on
<details>, not a knob — it is the browser’s behaviour, and the whole reason
this component needs no script.
Keyboard contract
Entirely native.
| Key | Behaviour |
|---|---|
Tab | Moves to each <summary> in document order |
Enter / Space | Toggles that panel |
Note that a disclosure group is not the ARIA tablist pattern: there are no
arrow-key semantics, and Tab visits every summary. That is correct for
<details> and is what a screen reader will describe.
Verified manually in VoiceOver and NVDA: not yet — do this before the component is marked done.
Accessibility notes
- Closed panels are genuinely closed. Their content is out of the accessibility tree, so a screen-reader user is not walked through collapsed prose.
- The summary is the button. Do not nest an interactive control inside
<summary>— it becomes a click target inside a click target. - Exclusive mode can close what a user is reading. If several panels are
likely to be wanted at once, leave the
nameoff and let them open independently.
Degradation
Baseline column from support.json — regenerate with pnpm support.
| Feature used | Baseline status | Behavior without it |
|---|---|---|
<details> / <summary> | widely | — |
name= grouping | newly | Panels open independently instead of exclusively. Everything still works; the group is just not mutually exclusive. |
::details-content | newly | No styling hook for the content wrapper; the panel’s own padding still applies. |
interpolate-size | limited | Instant open and close instead of an animated height. Content is never hidden or delayed. |
Every degradation here is cosmetic. The component’s function — disclose and
hide content, by keyboard or pointer — comes from <details>, which is Baseline
widely available and has been for years.