# Installation

> Install commands for every package, the three consumption modes with working snippets, the manual register escape hatch, the stylesheets and the build report.

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

Install the packages, add the Vite plugin, import the stylesheets, then write the markup.

## Requirements

| Tool | Range | Notes |
| --- | --- | --- |
| Node | `^20.19.0 \|\| >=22.12.0` | For the build (the `engines` of `@svelte-lean/vite`). The repository needs `>=22.18.0` and develops on `22.23.4`. |
| Svelte | `^5.20.0` | Peer of `@svelte-lean/table`; the primitives have no framework dependency. Tested with `5.57.1`. |
| Vite | `^6.0.0 \|\| ^7.0.0 \|\| ^8.0.0` | Peer of `@svelte-lean/vite`. Tested with `7.3.6` and, nightly, the latest release; Vite 6 is in the range and not tested. |
| TypeScript | 5 | `moduleResolution` `bundler` or `node16`. SvelteKit is optional. |

Read from the package files at build time; the full matrix is on [Compatibility](https://svelte-lean.vercel.app/docs/compatibility.md).

## Packages

```bash
npm install @svelte-lean/primitives
```

```bash
npm install @svelte-lean/styles
```

```bash
npm install --save-dev @svelte-lean/vite
```

```bash
npm install @svelte-lean/table
```

```bash
npm install @svelte-lean/query
```

- Every primitive is a subpath export of `@svelte-lean/primitives`.
- `@svelte-lean/styles` is optional and ships CSS only.
- `@svelte-lean/core` comes with the primitives package.
- `@svelte-lean/query` (server state) is independent of the others and versioned on its own: [Client setup](https://svelte-lean.vercel.app/query/client.md).
- All packages are ESM-only with strict `exports` maps.

## Vite plugin

`vite.config.ts`

```ts
// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { svelteLean } from '@svelte-lean/vite';
import { defineConfig } from 'vite';

export default defineConfig({
	plugins: [svelteLean(), sveltekit()]
});
```

`vite.config.ts`

```ts
// vite.config.ts, plain Vite without SvelteKit
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { svelteLean } from '@svelte-lean/vite';
import { defineConfig } from 'vite';

export default defineConfig({
	plugins: [svelteLean(), svelte()]
});
```

- Place `svelteLean()` before `sveltekit()`, so the scanner reads the source before any preprocessor.
- In development it keeps `@svelte-lean/primitives` and `@svelte-lean/core` out of dependency pre-bundling, so the first page with a behavior does not force a reload.
- Server transforms are skipped (`ssr: 'skip'`); registration modules touch neither `document` nor `window` at evaluation. Every route of this site is prerendered with this configuration.

## Styles

`+layout.svelte`

```ts
// src/routes/+layout.svelte
import '@svelte-lean/styles/tokens.css'; // required by every primitive file
import '@svelte-lean/styles/base.css'; // required by every primitive file
import '@svelte-lean/styles/dialog.css';
import '@svelte-lean/styles/tabs.css';
import '@svelte-lean/styles/menu.css'; // imports popover.css itself
```

```ts
// Convenience only: every stylesheet, for prototypes
import '@svelte-lean/styles';
```

- Every file declares the same cascade layer order, so the order between primitive files does not matter.
- Dependencies: `menu.css` and `menubar.css` need `popover.css`; `button-group.css` needs `button.css`. Each primitive page lists its stylesheets in order.
- Selectors are wrapped in `:where()`: any unlayered rule of the application wins.
- Dark values apply under `prefers-color-scheme: dark` or `data-theme="dark"` on the root.

## First component

### Native markup

A Tier 0 primitive is platform markup: nothing is imported, the browser owns the behavior, and the `data-slean` attributes are hooks for the stylesheet and development validation.

`settings.svelte`

```html
<button type="button" commandfor="settings" command="show-modal">Settings</button>

<dialog id="settings" data-slean="dialog" aria-labelledby="settings-title">
	<header>
		<h2 id="settings-title">Settings</h2>
	</header>
	<div>…</div>
	<footer>
		<button type="button" commandfor="settings" command="close">Close</button>
	</footer>
</dialog>
```

The native-only fixture ships 0 B of Svelte Lean JavaScript. Missing platform features are covered on [Browser support](https://svelte-lean.vercel.app/docs/browser-support.md).

### Behavior markup

Tabs need JavaScript the browser does not provide. Write the contract markup with a static marker; the plugin appends the registration import to the compiled module.

`+page.svelte`

```svelte
<!-- src/routes/settings/+page.svelte -->
<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>
```

`+page.svelte, compiled`

```js
// What the compiled module of that page receives from the plugin, appended after the
// compiled code (imports are hoisted, so it evaluates first):
import '@svelte-lean/primitives/tabs/register';
```

A hundred files with tabs still bundle one registration module. Tabs-only fixture: 1839 B brotli.

## Manual registration

Without the plugin, with a dynamic marker such as `data-slean={kind}`, or outside Vite, import the registration module once on the client.

`+layout.svelte`

```ts
// src/routes/+layout.svelte (or any module that runs once on the client)
import '@svelte-lean/primitives/tabs/register';
```

It is the module the plugin injects, and registration is idempotent, so mixing both is safe. Manual-registration fixture: 1839 B brotli, plugin path: 1839 B brotli, with the same module set asserted by both fixtures.

## Events in Svelte

Behaviors emit cancelable DOM events on their root (`slean:change`, `slean:select`, `slean:toggle`, and the others each contract lists). In Svelte 5, `onslean:change={…}` is a listener on that element, removed with it.

`+page.svelte`

```svelte
<!-- src/routes/settings/+page.svelte -->
<script lang="ts">
	let subscribed = $state(false);
</script>

<div
	data-slean="tabs"
	data-slean-value="account"
	onslean:change={(event) => {
		// detail is the union of every change detail; behavior narrows it.
		if (event.detail.behavior !== 'tabs') return;
		if (event.detail.value === 'billing' && !subscribed) event.preventDefault();
	}}
>
	…
</div>
```

Two one-time settings:

- The types-only entry `@svelte-lean/primitives/svelte-elements` types `event.detail` (narrowed by its `behavior` field) and fails `svelte-check` on a misspelled event. It also types `data-slean` where attributes are checked as props, such as rest props forwarded to an element.
- The compiler warns on every attribute name with a colon (`attribute_illegal_colon`). `@svelte-lean/vite` drops it for `onslean:` in builds and the dev server; `svelte-check` reads `svelte.config.js`, so compose the same filter there.

`src/app.d.ts`

```ts
// src/app.d.ts
/// <reference types="@svelte-lean/primitives/svelte-elements" />

declare global {
	namespace App {}
}

export {};
```

`svelte.config.js`

```js
// svelte.config.js
import { withSleanWarningFilter } from '@svelte-lean/vite';

export default {
	compilerOptions: {
		// onslean:change listens to slean:change; the compiler warns about every attribute name
		// with a colon. The Vite plugin drops the warning for onslean: in builds; svelte-check
		// reads this file. Pass your own filter to compose it.
		warningFilter: withSleanWarningFilter()
	}
};
```

## Table

A Svelte component with three required props and a stylesheet; no runtime, no plugin.

`members.svelte`

```svelte
<script lang="ts">
	import { DataTable, type Column } from '@svelte-lean/table';
	import '@svelte-lean/table/style.css';

	type Member = { id: number; name: string; revenue: number };
	const data: Member[] = [
		{ id: 1, name: 'Amara Okafor', revenue: 1200 },
		{ id: 2, name: 'Jonas Lindqvist', revenue: 9119 }
	];
	const columns: Column<Member>[] = [
		{ id: 'name', header: 'Member' },
		{ id: 'revenue', header: 'Revenue', align: 'end' }
	];
</script>

<DataTable {data} {columns} getRowId={(row) => row.id} />
```

Grouping, pivot, clipboard, CSV export, server data and persisted state are separate entry points. Next: [Getting started](https://svelte-lean.vercel.app/table/getting-started.md).

## Owned styles

Copy a primitive's stylesheet into the application and edit it; the behavior stays in the package, so runtime accessibility fixes still arrive through npm (ADR 0005). There is no `slean add` command yet.

```bash
# Mode C today: copy the file and own it. The behavior stays in the package.
cp node_modules/@svelte-lean/styles/css/tabs.css src/lib/styles/tabs.css
```

`+layout.svelte`

```ts
// The copied file keeps depending on the tokens; the behavior is still registered
// by the plugin (or by the manual import), not copied.
import '@svelte-lean/styles/tokens.css';
import '@svelte-lean/styles/base.css';
import '$lib/styles/tabs.css';
```

Modes, layers and variants: [Styling](https://svelte-lean.vercel.app/styling.md).

## Build verification

A production build writes `node_modules/.svelte-lean/manifest.json`: each behavior found, its module and the files that use it. `report: true` prints a summary.

`vite.config.ts`

```ts
// vite.config.ts
export default defineConfig({
	plugins: [svelteLean({ report: true }), sveltekit()]
});
```

`Build output`

```bash
svelte-lean build report
  native-only  button, dialog
  runtime      tabs -> @svelte-lean/primitives/tabs/register
                 src/routes/settings/+page.svelte
  unknown      none
  dynamic      none
  manifest     node_modules/.svelte-lean/manifest.json
```

The report lists what the scan saw; sizes and listener counts come from the consumer fixtures. The [proof](https://svelte-lean.vercel.app/proof.md) page shows this site's manifest.
