sveltelean Primitives
Versionv0.2.0 GitHub

Example

Exclusive group and a standalone item
Shipping
Orders ship within two days.
Returns
Thirty days, with the original packaging.
Notes
Starts open and is not part of the exclusive group.
<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>
Tier 0: the two sources differ only by the stylesheet import. The first two items share name="faq", so opening one closes the other; the third starts open. Everything here works with page JavaScript disabled.

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/styles
app.css or +layout.svelte
import '@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

PartElementdata-slean-partRequiredNotes
root<details>–yesdata-slean="disclosure" marks it for the styles. name groups siblings exclusively.
summary<summary>–yesThe first child of the details element.
contentthe element after the summarycontentstyles onlyPadding 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 the open attribute; 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 a name.
  • No disabled state exists. Text direction is native; the stylesheet draws the marker with logical properties, which the RTL checkbox above shows.

Keyboard

KeyWhenResult
Enter/Spacefocus on the summaryToggles open (native)
Tab/Shift+TabanywhereFocuses the summary; the content is reachable when open (native)

Platform features

FeatureBaselineOutside the target
<details>, <summary>, the toggle eventWidely availableNot applicable within the support policy
name for exclusive groupsNewly available since September 2024 (Chrome 120, Safari 17.2, Firefox 130)Siblings do not close each other
::details-content, interpolate-sizeUsed by the styles only where supportedThe 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:

TokenDefault (light)Applies to
--slean-bordervar(--slean-neutral-6)border-block-end
--slean-space-30.75remgap
--slean-control-height-md2.25remmin-block-size
--slean-space-20.5rempadding-block
--slean-font-weight-medium500font-weight
--slean-icon-chevron-downurl("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-normal160mstransition
--slean-easecubic-bezier(0.2, 0, 0, 1)transition
--slean-focus-ringvar(--slean-focus-ring-width) solid var(--slean-focus-ring-color)outline
--slean-focus-ring-offset2pxoutline-offset
--slean-fgvar(--slean-neutral-12)color
--slean-space-41rempadding-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:

app.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.

options.svelte
<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

Source