Accordion
Collapsible sections with one or many open at a time.
A vertically stacked set of collapsible sections. Each item is a heading-wrapped `<button>` toggle (label + optional hint + a chevron that rotates 180° on open) over an animated content `region`. Single-open by default; `multiple` lets several stay open. Works uncontrolled (`defaultValue`) or controlled (bindable `value` + `onChange`). The reveal uses the `--ss-dur` / `--ss-ease` motion tokens and collapses instantly under `prefers-reduced-motion`.
Demo
Usage
<script>
import { Accordion } from 'dssoca';
const items = [
{ id: 'shipping', label: 'Shipping' },
{ id: 'returns', label: 'Returns', hint: '30 days' },
];
</script>
<Accordion {items} defaultValue="shipping">
{#snippet panel(item)}
<p>Content for {item.label}.</p>
{/snippet}
</Accordion>Each header is a real `<button>` with `aria-expanded` + `aria-controls`; the panel is a `role="region"` labelled by its header (`aria-labelledby`). Enter/Space toggle; ArrowUp/ArrowDown move between headers (wrapping), Home/End jump to the first/last. Disabled sections use `aria-disabled` so they stay reachable by keyboard but do not toggle. The toggle chevron is the shared `Icon` `chevron` glyph (DS-0110) — `aria-hidden`, inside the header button — rotating 180° on open; the Accordion resolves its size once and passes it explicitly to that Icon (DS-0111), so a sm/md/lg Accordion gets a matching chevron, text, and spacing. The panel content is laid out as an always-padded block inside the grid-rows reveal clip, so it animates open as a stable padded block with no mid-animation reflow (DS-0115). Use `overflow="truncate"` to keep a long header label on a single line with an ellipsis (default `wrap`); a custom `header` snippet owns its own overflow handling. Zero border-radius; padding/typography scale with the size axis. WCAG 2.2 AA.
HTML
interactive with vanilla.js The exact markup the component renders, generated at build time. Use it with dssoca/vanilla.css+ dssoca/vanilla.js — see Plain HTML & CSS.
<div class="ss-accordion" data-overflow="wrap">
<div class="item">
<div role="heading" aria-level="3" class="heading">
<button type="button" class="head" id="acc-h-a" aria-expanded="false" aria-controls="acc-p-a">
<span class="title">Alpha</span>
<span class="hint">first</span>
<span class="chevron" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" class="ss-icon" style="width:var(--ss-icon);height:var(--ss-icon)" aria-hidden="true" focusable="false">
<path d="M8 10l4 4 4-4"/>
</svg>
</span>
</button>
</div>
<div class="panel" id="acc-p-a" role="region" aria-labelledby="acc-h-a" hidden="">
<div class="panel-clip">
<div class="panel-inner">
<p>Panel content.</p>
</div>
</div>
</div>
</div>
<div class="item">
<div role="heading" aria-level="3" class="heading">
<button type="button" class="head" id="acc-h-b" aria-expanded="false" aria-controls="acc-p-b">
<span class="title">Beta</span>
<span class="chevron" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" class="ss-icon" style="width:var(--ss-icon);height:var(--ss-icon)" aria-hidden="true" focusable="false">
<path d="M8 10l4 4 4-4"/>
</svg>
</span>
</button>
</div>
<div class="panel" id="acc-p-b" role="region" aria-labelledby="acc-h-b" hidden="">
<div class="panel-clip">
<div class="panel-inner">
<p>Panel content.</p>
</div>
</div>
</div>
</div>
</div>vanilla.js toggles the panel referenced by each header's aria-controls (single-open; add data-ss-multiple on the root to allow several) and roves focus with Arrow/Home/End. Emits ss:change.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | AccordionItem[] | — | Required. Sections — each `{ id, label, hint?, disabled? }`. |
panel | Snippet<[AccordionItem]> | — | Required. Renders the content for each section; receives the item. |
header | Snippet<[AccordionItem]> | — | Optional custom header content (replaces the default label + hint). |
multiple | boolean | false | Allow several sections open at once; otherwise opening one closes the others. |
value | string | string[] | — | Controlled, bindable open state — an id (single) or list of ids (`multiple`). |
defaultValue | string | string[] | — | Initial open section(s) when uncontrolled. |
onChange | (value) => void | — | Fired with the new open value whenever a section toggles. |
overflow | 'truncate' | 'wrap' | 'wrap' | How a long header label behaves when it exceeds the available width. `wrap` (default) flows onto multiple lines; `truncate` keeps it on a single line with an ellipsis. Applies to the default header markup only. |
headingLevel | 1 | 2 | 3 | 4 | 5 | 6 | 3 | Heading level wrapping each header button (document outline). |
idBase | string | — | Base id used to namespace the generated header/panel ids. Defaults to a stable per-instance id (from `$props.id()`) so server- and client-rendered IDs match (SSR-safe). |
size | 'sm' | 'md' | 'lg' | — | Per-instance size override; inherits the ancestor `data-size-variant` when unset. |