Primitives Layout and display
Disclosure
A native <details> with a <summary>; exclusive accordions use the name attribute. The browser owns the open state and the toggle; the package ships the stylesheet, a content part, the contract, and no runtime. Tier 0.
- Tier
- 0 · Native
- Behavior JS
- 0 B brotli · 0 B gzip ·
fixtures/· methodresults.json (native-only) - Platform features
<details>,<summary>,name,toggle,::details-content- Shared listeners
- none
- Per-instance listeners
- none
- Lazy state
- none
- Native base
<details> + <summary>
On this page
Example
Shipping
Returns
Notes
<details data-slean="disclosure" name="faq">
<summary>Shipping</summary>
<div data-slean-part="content">Orders ship within two days.</div>
</details>
<details data-slean="disclosure" name="faq">
<summary>Returns</summary>
<div data-slean-part="content">Thirty days, with the original packaging.</div>
</details>
<details data-slean="disclosure" open>
<summary>Notes</summary>
<div data-slean-part="content">Starts open and is not part of the exclusive group.</div>
</details><script lang="ts">
// Nothing to import: the browser owns the details element. The stylesheet is optional.
import '@svelte-lean/styles/disclosure.css';
</script>
<details data-slean="disclosure" name="faq">
<summary>Shipping</summary>
<div data-slean-part="content">Orders ship within two days.</div>
</details>
<details data-slean="disclosure" name="faq">
<summary>Returns</summary>
<div data-slean-part="content">Thirty days, with the original packaging.</div>
</details>
<details data-slean="disclosure" open>
<summary>Notes</summary>
<div data-slean-part="content">Starts open and is not part of the exclusive group.</div>
</details>Why this implementation exists
A disclosure is a button that shows and hides a region and announces its expanded state. <details> and <summary> are that: the summary is exposed
as a button with an expanded state, Enter and Space toggle it, the open attribute
is the state and the toggle event reports changes. The name attribute
makes siblings exclusive, which is the whole of an accordion controller. Svelte Lean therefore
ships no open store, no accordion controller and no animation runtime (ADR 0001);
the height transition is CSS on ::details-content where the browser supports it.
The browser owns
- the open state (the open attribute) and the toggle event
- Enter and Space on the summary
- exclusive groups through the name attribute
- the summary as a button with an expanded state for assistive technology
Svelte Lean owns
- disclosure.css: summary, marker, content padding and the height transition
- the contract and the DisclosurePart type
- 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/tokens.css';
import '@svelte-lean/styles/base.css';
import '@svelte-lean/styles/disclosure.css';Use a disclosure for content the reader may not need: answers, advanced options, long lists.
Give sibling items one name for an accordion where only one section is open at a
time; leave it off when several may be open. Render open on items that should start
expanded. <details> has no disabled state; do not add a fake one, render the content
statically instead.
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <details> | – | yes | data-slean="disclosure" marks it for the styles. name groups siblings exclusively. |
| summary | <summary> | – | yes | The first child of the details element. |
| content | the element after the summary | content | styles only | Padding and, where ::details-content is supported, the height animation. |
Runtime profile
The runtime block reads the tier and the events from the contract and the bytes from the
native-only consumer fixture: a production Vite build with the Vite plugin whose module graph
contains no @svelte-lean/core or @svelte-lean/primitives module. The
fixture's markup is button, dialog and popover; the disclosure path is proven the same way by
construction: packages/primitives/src/disclosure has no behavior, validate or
register module, the plugin maps disclosure to no module, and the playground's bundle
spec asserts that the native page, which includes this markup, loads no behavior runtime. Nothing
is attached at hydration.
Accessibility contract
- A
<details>whose first child is a<summary>. State is theopenattribute; nothing is mirrored. - No ARIA attribute is required: the summary is exposed as a button with an expanded state and the content is reachable when open.
- Exclusive accordion: sibling
<details>sharing aname. - No disabled state exists. Text direction is native; the stylesheet draws the marker with logical properties, which the RTL checkbox above shows.
Keyboard
| Key | When | Result |
|---|---|---|
| Enter/Space | focus on the summary | Toggles open (native) |
| Tab/Shift+Tab | anywhere | Focuses the summary; the content is reachable when open (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) | Siblings do not close each other |
::details-content, interpolate-size | Used by the styles only where supported | The content appears without a transition |
Without JavaScript
Fully functional, exclusive groups included. The playground's native page runs the pointer and keyboard toggle assertions and the exclusive-name assertion with page JavaScript disabled.
Server rendering
Static HTML. Render open on items that start expanded; the server output is the final
state and needs no correction on the client.
Before hydration
The delayed-hydration test clicks a summary and asserts the open attribute while every
script response is held back. Hydration attaches nothing to a details element and leaves an item the
reader opened before hydration open.
Styling
disclosure.css hides the platform marker, draws a chevron on summary::after that rotates on [open], and animates the height through ::details-content with interpolate-size: allow-keywords where the browser
supports them; elsewhere the content appears without a transition. The tokens it reads and the states
it targets:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-border | var(--slean-neutral-6) | border-block-end |
--slean-space-3 | 0.75rem | gap |
--slean-control-height-md | 2.25rem | min-block-size |
--slean-space-2 | 0.5rem | padding-block |
--slean-font-weight-medium | 500 | font-weight |
--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-ease | cubic-bezier(0.2, 0, 0, 1) | transition |
--slean-focus-ring | var(--slean-focus-ring-width) solid var(--slean-focus-ring-color) | outline |
--slean-focus-ring-offset | 2px | outline-offset |
--slean-fg | var(--slean-neutral-12) | color |
--slean-space-4 | 1rem | padding-block |
State selectors the stylesheet targets, all from the platform or ARIA: ::details-content, :focus-visible, :hover, [open].
Replacing the marker is one unlayered rule in your own CSS:
/* The stylesheet hides the platform marker and draws a chevron on summary::after.
* Replace it in your own CSS; the rule below wins over the package layer. */
[data-slean='disclosure'] > summary::after {
content: '+';
border: 0;
rotate: none;
translate: none;
}
[data-slean='disclosure'][open] > summary::after {
content: '−';
}Controlled integration
The open attribute is the state. Svelte binds it with bind:open; the
native toggle event fires after every change, including one caused by an exclusive
sibling. The package mirrors nothing and dispatches no slean:* event for a disclosure;
a Svelte adapter is not built and is not needed for this primitive.
<script lang="ts">
// The open attribute is the state. Svelte binds it directly; the toggle event fires after
// every change, including one made by an exclusive sibling.
let open = $state(false);
</script>
<details data-slean="disclosure" bind:open ontoggle={() => console.log('open:', open)}>
<summary>Advanced options</summary>
<div data-slean-part="content">…</div>
</details>Compatibility notes
<details>, <summary> and the toggle event are
Baseline widely available. name for exclusive groups is newly available since
September 2024 (Chrome 120, Safari 17.2, Firefox 130); where it is missing, siblings do not
close each other and everything else works. ::details-content and interpolate-size are used by the stylesheet only where supported. The policy is on Browser support.
Testing
apps/playground/tests/primitives/native.spec.tsPlaywright, with page JavaScript enabled and disabledapps/playground/tests/primitives/a11y.spec.tsaxe on every playground page and on open dialog, popover and menu statesfixtures/native-onlyconsumer build asserting that no behavior runtime ships (invariant A)packages/primitives/scripts/size.mjsthe native-only build that must contain no runtime
Source
packages/primitives/src/disclosure/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/disclosure/contract.tstyped constants: name, tier, base, parts, options, eventspackages/styles/css/disclosure.cssthe optional stylesheet