# Compared with raw Svelte

> Raw Svelte can implement every primitive; what the package adds is a tested contract, shared runtime, discovery and styles, never a claim that Svelte is slow.

Page: https://svelte-lean.vercel.app/docs/why-not-raw-svelte · Markdown: https://svelte-lean.vercel.app/docs/why-not-raw-svelte.md · Index: https://svelte-lean.vercel.app/llms.txt

Raw Svelte can implement every primitive here; the package saves solving and testing the same interaction again in every project.

## Comparison

| Concern | Raw Svelte | Svelte Lean |
| --- | --- | --- |
| Keyboard, focus, RTL, disabled, ARIA | Written and tested per project | Written once in a contract, tested in a browser |
| Handlers | Per instance | One `click` and one `keydown` for every tabs root, 1839 B brotli with the kernel |
| Markup validation | None | Console warning naming the trigger with a missing panel |
| Styles and tokens | Yours | Per primitive, with dark mode, forced colors and reduced motion |
| Application state | `$state` | `$state` (Tier 3 by design) |

## What raw Svelte gives you

Everything, compiled to efficient code; Svelte Lean is built for Svelte 5, SvelteKit and Vite and does not claim Svelte's event model needs fixing. The sketch below 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, and a browser test for each.

`tabs.svelte`

```svelte
<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>
```

## What the package adds

`tabs.svelte`

```html
<!-- 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>
```

- Build-time discovery: the behavior is added only for markup that uses it.
- Browser support knowledge for each native primitive.
- One protocol, event set and vocabulary across every primitive and the table.
- Measured cost of each piece, with its method.

## Where raw Svelte stays

- Application state: a tab in the URL, a dialog driven by domain state, a sort tied to a server query. The table's state is one plain object the application binds.
- An interaction unique to the application, or a native element needing nothing beyond the platform (see [plain HTML](https://svelte-lean.vercel.app/docs/why-not-html.md)).
