Components
Accordion
Collapsible sections built on native details and summary elements without JavaScript.
A <ui-accordion> wraps native <details> and <summary> elements. The class form <div class="ui-accordion"> is equivalent: the tag is CSS-only without JavaScript. Expand and collapse states, keyboard interactions, and animations (via ::details-content) run natively in the browser.
Default
Loading components…
<ui-accordion> <details class="border-b py-sm"> <summary class="font-strong hover:text-muted-foreground"> What is an accordion? <svg 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 class="pb-sm"> <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 class="border-b py-sm"> <summary class="font-strong hover:text-muted-foreground"> How does it work? <svg 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 class="pb-sm"> <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 class="border-b py-sm"> <summary class="font-strong hover:text-muted-foreground"> Why use it? <svg 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 class="pb-sm"> <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 | .ui-accordion on <details>/<summary>) * * @layer variables, reset, components * @requires layers.css, _variables.css * @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) */@layer variables { :root { --ui-accordion-icon-size: var(--step-4); --ui-accordion-icon-transform-open: rotate(-180deg); }}@layer reset { /* =========================================================================== DETAILS & SUMMARY: native open/close transition + summary marker reset - In the reset layer so the :where(ui-accordion, .ui-accordion) component styling below wins. =========================================================================== */ :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 zazz.components { :where(ui-accordion, .ui-accordion) { display: grid; inline-size: 100%; flex: 1; } :where(ui-accordion, .ui-accordion) > details { display: grid; flex: 1 0 auto; } :where(ui-accordion, .ui-accordion) > details > summary { display: flex; align-items: center; justify-content: space-between; /* ring renders as box-shadow; the ring widths animate from 0 so the ring grows in smoothly on focus */ --_ring-offset-width: 0px; --_ring-width: 0px; box-shadow: 0 0 0 var(--_ring-offset-width) var(--ring-offset-color), 0 0 0 calc(var(--_ring-offset-width) + var(--_ring-width)) var(--_ring, var(--ring)); /* transparent twin of the ring for forced-colors / high-contrast modes */ outline: var(--outline-width) var(--outline-style) transparent; outline-offset: var(--outline-offset); transition: var(--default-transition); } :where(ui-accordion, .ui-accordion) > details > summary:focus-visible { --_ring-offset-width: var(--ring-offset-width); --_ring-width: var(--ring-width); outline-color: transparent; } :where(ui-accordion, .ui-accordion) summary > svg { 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, .ui-accordion) > details[open] > summary > svg { transform: var(--ui-accordion-icon-transform-open); }}Structure each panel as a <details class="border-b"> containing a <summary class="font-strong hover:text-muted-foreground"> with an SVG chevron, followed by a body <div>. Multiple panels can open simultaneously. Give <details> items matching name attributes for mutual exclusion.
Customize using tokens: --ui-accordion-icon-size and --ui-accordion-icon-transform-open.