Docs Architecture
Distribution
Take what you need. Ship what you use. What is installed, what is copied into the application and what reaches the production bundle are three separate questions, and the packages are built so the answers can differ.
On this page
Three questions
- What is installed in
node_modules? One package per product. - What source is copied into the application? Nothing by default; stylesheets when the application chooses to own them.
- What JavaScript and CSS is shipped to production? The registration modules of the behaviors the markup uses, the stylesheets that were imported, and nothing else.
An application can install @svelte-lean/primitives, use seven of its primitives,
and ship no Svelte Lean behavior JavaScript at all: the native-only production fixture emits a
Svelte Lean chunk of 0 B.
Consumption modes
Mode A: native, copy only
Plain platform markup that needs no package at all. The best implementation of a native primitive is zero runtime modules, zero registrations, zero components and zero imports, and the documentation shows that path first rather than hiding it behind a wrapper.
<button type="button" commandfor="settings" command="show-modal">Settings</button>
<dialog id="settings" data-slean="dialog" aria-labelledby="settings-title">
<header data-slean-part="header">
<h2 id="settings-title" data-slean-part="title">Settings</h2>
</header>
<div data-slean-part="body">…</div>
<footer data-slean-part="footer">
<button type="button" commandfor="settings" command="close">Close</button>
</footer>
</dialog>Mode B: package-managed behavior
For a behavior the browser lacks, the markup carries a static marker and @svelte-lean/vite injects only the matching registration module. A manual import is the
documented and tested escape hatch and behaves identically.
// 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()]
});// src/routes/+layout.svelte (or any module that runs once on the client)
import '@svelte-lean/primitives/tabs/register';Mode C: owned styles
The stylesheets are plain files the application may copy and edit. The behavior engine is never
copied: it is accessibility-sensitive, and a focus bug fixed upstream must reach every project
through npm. The CSS is yours to own; the behavior stays a maintained dependency. A slean add command for the copy is future work.
/* Mode C without any package CSS: target the protocol and the platform state directly */
[data-slean='tabs'] [role='tab'][aria-selected='true'] {
border-bottom: 2px solid currentColor;
}
[data-slean='dialog']::backdrop {
background: rgb(0 0 0 / 0.4);
}Package granularity
One product package, never one npm package per primitive. Behaviors are subpath exports: the
plain entry holds types, the contract and pure helpers; the register entry is the one
side effect. The root entry exports metadata and contracts and never registers a behavior.
import type { TabsChangeDetail } from '@svelte-lean/primitives/tabs'; // types, pure helpers
import '@svelte-lean/primitives/tabs/register'; // the side effect: registers tabs once
import { BEHAVIORS } from '@svelte-lean/primitives'; // metadata and contracts; registers nothingThe exports map of @svelte-lean/primitives, read from its package.json at build time: ., ./button, ./dialog, ./popover, ./disclosure, ./checkbox, ./switch, ./radio-group, ./tabs, ./tabs/register, ./menu, ./menu/register, ./listbox, ./listbox/register, ./combobox, ./combobox/register, ./date-field, ./calendar, ./calendar/register, ./date-picker, ./date-picker/register, ./input, ./field, ./select, ./select/register, ./segmented, ./rating, ./slider, ./otp-field, ./file-field, ./color-field, ./autocomplete, ./alert-dialog, ./drawer, ./accordion, ./card, ./separator, ./avatar, ./badge, ./tag, ./kbd, ./carousel, ./alert, ./progress, ./meter, ./spinner, ./skeleton, ./breadcrumb, ./pagination, ./steps, ./toggle, ./toggle/register, ./toggle-group, ./toggle-group/register, ./toolbar, ./toolbar/register, ./range-slider, ./range-slider/register, ./number-field, ./number-field/register, ./tree, ./tree/register, ./tooltip, ./tooltip/register, ./toast, ./toast/register, ./button-group, ./scroll-area, ./descriptions, ./timeline, ./empty, ./splitter, ./splitter/register, ./context-menu, ./context-menu/register, ./file-drop, ./file-drop/register, ./hover-card, ./hover-card/register, ./menubar, ./menubar/register, ./svelte-elements.
The table follows the same rule with larger units: the root entry is <DataTable /> and createTable(); grouping, pivot, editing,
clipboard, export, server data sources and persisted state are entry points that never load
unless imported. Its exports: ., ./core, ./grouping, ./pivot, ./export, ./editing, ./state, ./server, ./clipboard, ./style.css.
import { DataTable, createTable, createRowModel } from '@svelte-lean/table';
import { groupBy, summarize } from '@svelte-lean/table/grouping';
import { pivot } from '@svelte-lean/table/pivot';
import { createHistory, applyEdit } from '@svelte-lean/table/editing';
import { copyRange, pasteRange } from '@svelte-lean/table/clipboard';
import { toCsv, downloadText } from '@svelte-lean/table/export';
import { createDataSource } from '@svelte-lean/table/server';
import { serializeState, restoreState } from '@svelte-lean/table/state';
import '@svelte-lean/table/style.css';sideEffects lists only the register modules and the CSS, so a bundler may drop everything
else that is unused and must keep a registration that was imported.
// packages/primitives/package.json: only the register modules are side effects
"sideEffects": ["./dist/*/register.js"]
// packages/table/package.json
"sideEffects": ["**/*.css"]Styles distribution
Granular entry points are primary: @svelte-lean/styles/tabs.css and one file per
primitive, with the tokens in a separate file that any theme can override. The monolithic @svelte-lean/styles entry exists for convenience only. The package ships CSS and no JavaScript;
every export is a CSS file, which its tests assert.
Shipping invariants
Five release-blocking invariants, each with the place it is tested against real production output. One has nothing to test yet and says so.
| Invariant | Where it is tested | Status in this build |
|---|---|---|
| A. Native-only pages ship no behavior runtime | fixtures/native-only; packages/primitives/scripts/size.mjs (native-only entry) | 31 assertions pass |
| B. Unused behaviors are absent from production output | fixtures/tabs-only, tabs-and-menu, manual-registration, table-basic | tabs-only: 43 assertions pass; tabs-and-menu: 43 assertions pass; manual-registration: 42 assertions pass; table-basic: 46 assertions pass |
| C. Instance count does not multiply shared listeners | apps/playground/tests/primitives/listeners.spec.ts (1 vs 1000 roots); packages/primitives/tests/tabs.test.ts | exact equality, release gate |
| D. Tier 2 state is lazy | packages/primitives/tests/combobox.test.ts ("lazy controller": untouched roots hold no state, 1000 roots, session count); apps/playground/tests/primitives/lazy.spec.ts (100 roots in Chrome) | exact assertion, release gate |
| E. Compatibility code is opt-in or target-selected | No test yet: no compatibility module exists. The first one adds a fixture proving the native path excludes it. | nothing to test |
| Fixture | Consumes | Svelte Lean chunk (brotli) |
|---|---|---|
| native-only | button, dialog, popover markup; the stylesheet; the plugin | 0 B |
| tabs-only | tabs markup; the plugin | 1521 B |
| manual-registration | tabs markup; the register module imported by hand; no plugin | 1521 B |
| tabs-and-menu | tabs and menu markup; one kernel | 2351 B |
| table-basic | <DataTable /> and its stylesheet; six optional modules asserted absent | 16829 B |
Each fixture is a Vite application built in production mode in its own process, with every @svelte-lean/* module isolated into one chunk so its bytes are never mixed with
Svelte's. The fixtures resolve the workspace packages through node_modules links.
Packed tarballs are tested separately with publint, a check of every exports target, a Node import of every subpath and a server render of the table (pack-smoke.mjs); the shipping invariants themselves are asserted on the workspace-linked fixture builds (fixtures/README.md).
Manifest and report
Discovery is acceptable magic because it is inspectable: the source markup is visible, the production build writes a manifest of every behavior it found and the module it injected, the build report prints the same summary, and the manual import exists. The manifest of this site's own build is on Build-time discovery.
svelteLean({
// A behavior of your own, or a replacement module for a built-in one.
behaviors: {
carousel: '$lib/behaviors/carousel/register',
tabs: '@acme/primitives/tabs/register'
}
});Escape hatches
Every abstraction has a lower-level path, and each is documented and tested.
| Abstraction | Escape hatch | Documented on |
|---|---|---|
| Automatic behavior discovery by the Vite plugin | A manual import '@svelte-lean/primitives/tabs/register'; the same module, the same bytes | /docs/installation#manual-registration |
| The built-in behavior table of the plugin | The behaviors option: your own registration module for a name, or false for no runtime | /docs/architecture/build-time-discovery#options |
| The document runtime | createRuntime({ root }) for a shadow root or an iframe; register the same definitions on it | /docs/architecture/build-time-discovery#the-shared-router |
| <DataTable /> | createTable(): the engine it renders, for a renderer of your own; @svelte-lean/table/core for the pure row model | /table/api |
| Uncontrolled table state | bind:state or onStateChange: one plain object the application owns, serializes and restores | /table/state-persistence |
| The package stylesheet | Your own CSS against the protocol attributes and platform state, or a copied file you own | /styling/owned-styles |
| Native positioning of popovers | Your own anchor-name and position-anchor rules; no compatibility positioning module exists | /primitives/popover |
| The declarative tab selection | activateTab(root, value) and the cancelable slean:change event | /docs/architecture/dom-protocol#events |
import { createTable } from '@svelte-lean/table';
// Below <DataTable />: the engine it renders, for a renderer of your own.
const table = createTable({
data: () => rows,
columns,
getRowId: (row) => row.id,
state: () => tableState,
onStateChange: (next) => (tableState = next)
});
table.toggleSort('revenue');
table.model.rows;Source
- ADR 0005, Take what you need, ship what you use
- primitives package.json and table package.json: the exports maps and
sideEffects - fixtures/: the consumer builds and their assertions
- pack-smoke.mjs: the packed-tarball test