Primitives Layout and display
Accordion
A group of native <details> elements that share a name, so one is open at a time. The browser owns the open state, the keyboard and the exclusivity; the package draws the group as one bordered list and adds a contract. Tier 0.
- Tier
- 0 · Native
- Behavior JS
- 0 B brotli · 0 B gzip ·
fixtures/· methodresults.json (native-only) - Platform features
<details name>,<summary>,toggle,::details-content,interpolate-size- Shared listeners
- none
- Per-instance listeners
- none
- Lazy state
- none
- Native base
<details name> group
On this page
Example
Shipping
Returns
Warranty
<div data-slean="accordion">
<details name="accordion-faq" open>
<summary>Shipping</summary>
<div data-slean-part="content">Orders ship within two working days from the Rotterdam warehouse.</div>
</details>
<details name="accordion-faq">
<summary>Returns</summary>
<div data-slean-part="content">Thirty days, with the original packaging.</div>
</details>
<details name="accordion-faq">
<summary>Warranty</summary>
<div data-slean-part="content">Two years on parts and labour, from the date of delivery.</div>
</details>
</div><script lang="ts">
// Nothing to import but the optional stylesheet: the browser owns the details elements.
import '@svelte-lean/styles/accordion.css';
</script>
<div data-slean="accordion">
<details name="accordion-faq" open>
<summary>Shipping</summary>
<div data-slean-part="content">Orders ship within two working days from the Rotterdam warehouse.</div>
</details>
<details name="accordion-faq">
<summary>Returns</summary>
<div data-slean-part="content">Thirty days, with the original packaging.</div>
</details>
<details name="accordion-faq">
<summary>Warranty</summary>
<div data-slean-part="content">Two years on parts and labour, from the date of delivery.</div>
</details>
</div>Why this implementation exists
An accordion is a list of disclosures where opening one closes the others. The platform has that as an attribute: <details> elements with the same name form an exclusive group, and the browser closes the open item when another opens. Enter and Space toggle an item, each summary is announced as a button with its expanded state, and find-in-page opens the item that holds a match. None of it needs a script.
Disclosure is the page for one <details>; its styles make every item a row with a rule under it. Accordion uses the same element and the same attribute and adds only a root: a bordered list with rules between the items, rounded ends and an inset focus ring. Pick Disclosure for items that stand alone and Accordion for a set that reads as one block.
The browser owns
- the open state of every item (the open attribute) and the toggle event
- closing the open item when another item with the same name opens
- Enter and Space on each summary, and the summary as a button with an expanded state
- opening a closed item when find-in-page matches its content
Svelte Lean owns
- accordion.css: the bordered list, the rules between items, the summary row, the chevron and the height transition
- the contract: one name per accordion, at most one item rendered open per name
- documentation
Usage
The markup needs no package. Install @svelte-lean/styles for the stylesheet; @svelte-lean/primitives adds the typed contract and nothing at runtime.
npm install @svelte-lean/stylespnpm add @svelte-lean/stylesyarn add @svelte-lean/stylesbun add @svelte-lean/stylesimport '@svelte-lean/styles/accordion.css';Give every accordion its own name: the browser groups all <details> of the document that share a value, wherever they are. Render open on at most one item of a group; with more, the browser keeps the first and closes the rest while parsing.
Leave name off when several items may be open. The exclusive group can always be closed completely; an item that must stay visible belongs outside the accordion.
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <div data-slean="accordion"> | – | yes | Direct children are the <details> items. No role and no state of its own. |
| item | <details name="…"> | – | yes | The same name on every item for one open at a time; no name for several. |
| summary | <summary> | – | yes | The first child of each item; the header the reader activates. |
| content | the element after the summary | content | styles only | Padding and the muted text color. The height transition does not need it. |
Runtime profile
Tier 0: the accordion has no behavior module, the Vite plugin maps accordion to no module, and the native-only fixture proves the path ships no Svelte Lean JavaScript.
Accessibility contract
- Each
<summary>is exposed as a button with an expanded state that controls its item; no ARIA attribute is needed. - Enter and Space toggle an item; Tab moves between summaries and into open content. The arrow-key movement between headers that the WAI-ARIA accordion pattern marks optional is not provided.
- A heading inside
<summary>is valid HTML, but screen readers differ in whether they still expose it as a heading, so the contract does not require one. <details>has no disabled state and an exclusive group can always be closed completely.
Keyboard
| Key | When | Result |
|---|---|---|
| Enter/Space | focus on a summary | Toggles its item (native) |
| Tab/Shift+Tab | anywhere | Moves between summaries and into open content (native) |
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
<details>, <summary>, the toggle event | Widely available | Not applicable within the support policy |
name for exclusive groups | Newly available since September 2024 (Chrome 120, Safari 17.2, Firefox 130) | Every item opens on its own |
Find-in-page opens a closed item | Chrome 97, Firefox 139, Safari 26.2 | The match inside a closed item is not found |
::details-content | Newly available since September 2025 (Chrome 131, Safari 18.4, Firefox 143) | The content appears without a transition |
interpolate-size: allow-keywords | Chrome 129; not in Firefox or Safari | The content appears without a transition |
Without JavaScript
Fully functional, exclusive groups included: the name attribute is read by the browser, not by a script.
Server rendering
Static HTML. Render open on the item that starts expanded; the server output is the final state.
Before hydration
Nothing is attached. An item the reader opens before hydration stays open after it.
Styling
accordion.css draws the root's border and radius, the rule between items, the summary row with its hover fill and inset focus ring, the chevron (two borders on summary::after), the content padding, and the height transition on ::details-content that runs where interpolate-size is supported. The tokens it reads:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-border | var(--slean-neutral-6) | border, border-block-start |
--slean-radius-lg | 0.875rem | border-radius, border-start-start-radius, border-start-end-radius, border-end-start-radius, border-end-end-radius |
--slean-surface | oklch(100% 0 0) | background |
--slean-fg | var(--slean-neutral-12) | color |
--slean-space-3 | 0.75rem | gap |
--slean-control-height-lg | 2.75rem | min-block-size |
--slean-space-2 | 0.5rem | padding-block |
--slean-space-4 | 1rem | padding-inline, padding-block |
--slean-font-weight-medium | 500 | font-weight |
--slean-duration-fast | 100ms | transition |
--slean-ease | cubic-bezier(0.2, 0, 0, 1) | transition |
--slean-icon-chevron-down | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E") | mask |
--slean-duration-normal | 160ms | transition |
--slean-focus-ring | var(--slean-focus-ring-width) solid var(--slean-focus-ring-color) | outline |
--slean-focus-ring-width | 2px | outline-offset |
--slean-muted | var(--slean-neutral-3) | background |
--slean-fg-muted | var(--slean-neutral-11) | color |
State selectors the stylesheet targets, all from the platform or ARIA: ::details-content, :first-child, :focus-visible, :hover, :last-child, [open].
Controlled integration
The open attribute of each item is its state. Bind it per item; an item closed by its exclusive sibling fires toggle, and the binding follows.
<script lang="ts">
// Each item's open attribute is its state; bind it. Opening one item of the group closes
// the other, and Svelte's binding follows through the toggle event.
const items = [
{ id: 'shipping', title: 'Shipping', text: 'Orders ship within two working days.' },
{ id: 'returns', title: 'Returns', text: 'Thirty days, with the original packaging.' }
];
let open: Record<string, boolean> = $state({ shipping: true, returns: false });
const current = $derived(items.find((item) => open[item.id])?.title ?? 'none');
</script>
<div data-slean="accordion">
{#each items as item (item.id)}
<details name="checkout-faq" bind:open={open[item.id]}>
<summary>{item.title}</summary>
<div data-slean-part="content">{item.text}</div>
</details>
{/each}
</div>
<p>Open: {current}</p>Compatibility notes
Exclusive groups are Baseline since September 2024 (Chrome 120, Safari 17.2, Firefox 130); in older browsers every item opens on its own. Find-in-page opens a closed item in Chrome 97, Firefox 139 and Safari 26.2. ::details-content is Baseline since September 2025, but the height animates only where interpolate-size exists, which is Chrome 129 and not Firefox or Safari; elsewhere the content appears without a transition.
Examples
Several open
Without a name each item opens and closes on its own; the root still draws one list.
Notifications
Privacy
Sessions
<!-- No name: every item opens and closes on its own. -->
<div data-slean="accordion">
<details open>
<summary>Notifications</summary>
<div data-slean-part="content">…</div>
</details>
<details open>
<summary>Privacy</summary>
<div data-slean-part="content">…</div>
</details>
</div>Testing
apps/playground/tests/primitives/display.spec.tsPlaywright: open state, exclusive groups, stretched links, roles and names, carousel scrolling, axepackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)
Source
packages/primitives/src/accordion/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/accordion/contract.tstyped constants: name, tier, base, parts, options, eventspackages/styles/css/accordion.cssthe optional stylesheet