API
Accordion
Disclosure groups on the native `<details>` element.
Installation
pnpm dlx zazz-ui add accordionnpx zazz-ui add accordionyarn dlx zazz-ui add accordionbunx zazz-ui add accordionRun zazz-ui init first if the project has no zazz/ folder yet.
Examples
What is an accordion?
A disclosure widget built from the native <details> and
<summary> elements. The open/close animation uses the
::details-content pseudo-element — no JavaScript.
How does it work?
Each item is a separate <details>, so several can stay open at once.
Click a summary or focus it and press Enter to toggle.
Why use it?
Keyboard support, state, and accessibility come from the platform — Zazz only styles the chrome through theme tokens.
<ui-accordion>
<details
style="
--py: 4;
--border-b: 1;
"
>
<summary
style="
--font-weight: strong;
--text: currentColor;
--text--hover: var(--color-muted-foreground);
"
>
What is an accordion?
<svg data-accordion-slot="marker" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="none" />
<polyline
points="208 96 128 176 48 96"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
</svg>
</summary>
<div style="--pb: 4">
<p>
A disclosure widget built from the native <code><details></code> and
<code><summary></code> elements. The open/close animation uses the
<code>::details-content</code> pseudo-element — no JavaScript.
</p>
</div>
</details>
<details
style="
--py: 4;
--border-b: 1;
"
>
<summary
style="
--font-weight: strong;
--text: currentColor;
--text--hover: var(--color-muted-foreground);
"
>
How does it work?
<svg data-accordion-slot="marker" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="none" />
<polyline
points="208 96 128 176 48 96"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
</svg>
</summary>
<div style="--pb: 4">
<p>
Each item is a separate <code><details></code>, so several can stay open at once.
Click a summary or focus it and press Enter to toggle.
</p>
</div>
</details>
<details
style="
--py: 4;
--border-b: 1;
"
>
<summary
style="
--font-weight: strong;
--text: currentColor;
--text--hover: var(--color-muted-foreground);
"
>
Why use it?
<svg data-accordion-slot="marker" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="none" />
<polyline
points="208 96 128 176 48 96"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
</svg>
</summary>
<div style="--pb: 4">
<p>
Keyboard support, state, and accessibility come from the platform — Zazz only styles the
chrome through theme tokens.
</p>
</div>
</details>
</ui-accordion>/**
* accordion.css: Accordion (ui-accordion | [data-ui~="accordion"] on <details>/<summary>;
* slot data-accordion-slot="marker" on the summary icon that rotates open)
*
* @layer variables, reset, components
* @requires _layers.css, _variables.css, _properties.css (--_focus-shadow)
* @uses ::details-content: native open/close block-size transition (reset layer)
* @uses interpolate-size: allow-keywords (from _variables.css; required for
* block-size: 0 -> auto transitions)
* @tokens --ui-accordion-* (@layer variables; none backs a utility)
*/
@layer variables {
:root {
--ui-accordion-icon-size: calc(var(--spacing) * 4);
--ui-accordion-icon-transform--open: rotate(-180deg);
}
}
@layer reset {
/* ===========================================================================
DETAILS & SUMMARY: native open/close transition + summary marker reset
=========================================================================== */
:where(details)::details-content {
--details-content-transition:
opacity var(--default-transition-duration) var(--default-transition-timing-function),
transform var(--default-transition-duration) var(--default-transition-timing-function),
block-size var(--default-transition-duration) var(--default-transition-timing-function),
content-visibility var(--default-transition-duration)
var(--default-transition-timing-function) allow-discrete;
block-size: 0;
overflow: hidden;
transition: var(--details-content-transition);
will-change: opacity, transform, block-size, content-visibility;
}
:where(details[open])::details-content {
block-size: auto;
}
:where(summary) {
display: flex;
text-decoration: none;
user-select: none;
list-style: none;
}
}
@layer ui.components {
:where(ui-accordion, [data-ui~="accordion"]) {
display: grid;
inline-size: 100%;
flex: 1;
}
:where(ui-accordion, [data-ui~="accordion"]) > details {
display: grid;
flex: 1 0 auto;
}
:where(ui-accordion, [data-ui~="accordion"]) > details > summary {
display: flex;
align-items: center;
justify-content: space-between;
/* The ring is published to --_focus-shadow so a --shadow utility on the
summary keeps it; the ring widths grow from 0 so the ring eases in. */
--_ring-offset-width: 0px;
--_ring-width: 0px;
--_focus-in: max(0, -1 * sign(var(--_focus-ring-w, 1px)));
--_focus-shadow:
0 0 0 calc(var(--_ring-offset-width) * (1 - var(--_focus-in))) var(--ring-offset-shadow-color),
0 0 0 calc((var(--_ring-offset-width) + var(--_ring-width)) * (1 - var(--_focus-in)))
var(--_ring-fill, var(--ring-shadow-color)),
inset 0 0 0 calc(var(--_ring-offset-width) * var(--_focus-in)) var(--ring-offset-shadow-color),
inset 0 0 0 calc((var(--_ring-offset-width) + var(--_ring-width)) * var(--_focus-in))
var(--_ring-fill, var(--ring-shadow-color));
box-shadow: var(--_focus-shadow, 0 0 #0000), var(--_shadow-resolved, 0 0 #0000);
/* transparent twin of the ring for forced-colors / high-contrast modes */
outline: abs(var(--_focus-ring-w, var(--outline-width))) var(--outline-style) transparent;
outline-offset: calc(
var(--outline-offset) - var(--_focus-in) *
(2 * var(--outline-offset) + abs(var(--_focus-ring-w, var(--outline-width))))
);
transition: var(--default-transition);
}
:where(ui-accordion, [data-ui~="accordion"]) > details > summary:focus-visible {
--_ring-offset-width: var(--ring-offset-width);
--_ring-width: abs(var(--_focus-ring-w, var(--ring-width)));
outline-color: transparent;
}
:where(ui-accordion, [data-ui~="accordion"]) summary > [data-accordion-slot="marker"] {
display: flex;
align-items: center;
justify-content: center;
inline-size: var(--ui-accordion-icon-size);
block-size: var(--ui-accordion-icon-size);
pointer-events: none;
transition: transform var(--default-transition-duration)
var(--default-transition-timing-function);
}
:where(ui-accordion, [data-ui~="accordion"])
> details[open]
> summary
> [data-accordion-slot="marker"] {
transform: var(--ui-accordion-icon-transform--open);
}
}Attributes
| Attribute | Kind | Values | Notes |
|---|---|---|---|
data-ui="accordion" | Identity | free value | Identity token in data-ui. |
<ui-accordion> | Tag form | free value | Tag form; styled and, when scripted, registered under this name. |
data-accordion-slot | Slot | marker | Slot: a part inside the primitive. |
Hooks
Set a hook on :root, on a subtree, or inline on one element. A state hook (--…--hover) themes that state only; the primitive keeps its other states.
| Hook | Default | State |
|---|---|---|
--ui-accordion-icon-size | calc(var(--spacing) * 4) | — |
--ui-accordion-icon-transform--open | rotate(-180deg) | open |