Docs Philosophy
Compared with raw Svelte
Raw Svelte can implement every primitive here. The value of the package is not that developers cannot write tabs; it is that they should not have to solve and test the same interaction problem again in every project.
On this page
What raw Svelte gives you
Everything. A dialog is a <dialog> in a component; tabs are a list of buttons
with a $state for the selection and a keydown handler. Svelte compiles that to efficient
code and delegates certain events itself. Svelte Lean does not claim Svelte's event model is slow
or that it needs fixing; the packages are built for Svelte 5, SvelteKit and Vite and would make no
sense without them.
<script lang="ts">
// A sketch of tabs in raw Svelte. It is not wrong; it is the start of a list:
// Home and End, RTL, manual activation, disabled triggers, roving tabindex after a
// cancelled change, nested tabs, tests for each of these in a browser.
let current = $state('account');
const tabs = ['account', 'security'];
function onkeydown(event: KeyboardEvent, index: number) {
if (event.key === 'ArrowRight') current = tabs[(index + 1) % tabs.length]!;
if (event.key === 'ArrowLeft') current = tabs[(index - 1 + tabs.length) % tabs.length]!;
}
</script>
<div role="tablist" aria-label="Settings">
{#each tabs as tab, index (tab)}
<button
type="button"
role="tab"
aria-selected={current === tab}
tabindex={current === tab ? 0 : -1}
onclick={() => (current = tab)}
onkeydown={(event) => onkeydown(event, index)}
>
{tab}
</button>
{/each}
</div>The sketch is not wrong. It is the start of a list: Home and End, RTL, manual activation,
disabled triggers, the roving tabindex after a cancelled change, nested tabs, the relationship
attributes, and a browser test for each. Every project that writes the sketch writes the list, or
ships without it.
What the package adds
<!-- The same widget on the protocol: the behavior is registered once per page and
tested once for everyone; the state stays in the attributes you see here. -->
<div data-slean="tabs" data-slean-value="account">
<div role="tablist" aria-label="Settings" data-slean-part="list">
<button type="button" id="tab-account" role="tab" aria-controls="panel-account"
aria-selected="true" tabindex="0" data-slean-part="trigger" data-slean-value="account">
Account
</button>
<button type="button" id="tab-security" role="tab" aria-controls="panel-security"
aria-selected="false" tabindex="-1" data-slean-part="trigger" data-slean-value="security">
Security
</button>
</div>
<div id="panel-account" role="tabpanel" aria-labelledby="tab-account"
data-slean-part="panel" data-slean-value="account">…</div>
<div id="panel-security" role="tabpanel" aria-labelledby="tab-security"
data-slean-part="panel" data-slean-value="security" hidden>…</div>
</div>- A tested behavior contract: the keyboard, focus, disabled, RTL and ARIA rules of the widget, written down and checked in a browser (tabs contract).
- Accessibility work done once: the roving
tabindex, the activation modes, the cancelable change, the validation that reports a missing panel in the browser console with the trigger value concerned. - Browser compatibility knowledge for the native primitives: which feature each depends on and what happens without it.
- A shared runtime instead of a handler per instance: one
clickand onekeydownlistener for every tabs root on the page, measured at 1521 B brotli including the kernel. - Build-time discovery, so the behavior is added for the markup that uses it.
- Styles and tokens per primitive, with dark mode, forced colors and reduced motion handled.
- The same protocol, events and vocabulary across every primitive and the table.
- Measurements with their method, so the cost of each piece is known before it is used.
Where raw Svelte stays
Application state stays in Svelte. The selected tab stored in the URL, a dialog controlled by
domain state, a table sort connected to a server query: these are Tier 3 by design, and the
package leaves them to $state and the application. The table's state is one plain object
the application binds. Svelte Lean removes interaction abstractions the platform makes unnecessary;
it does not remove Svelte from the interaction.
When to write it yourself
When the interaction is unique to the application and no contract fits, or when it is a native element with no requirement beyond the platform's own behavior and no need for the stylesheet. The plain HTML page covers the second case, and the answer there is the same: sometimes you should.