# Svelte Lean > Browser-native UI for Svelte. Semantic HTML first, zero runtime where the browser owns the behavior, small shared behaviors where it does not. Two products: Svelte Lean Primitives (@svelte-lean/primitives) and Svelte Lean Table (@svelte-lean/table). Every page in one file: https://svelte-lean.vercel.app/llms-full.txt How to use these files with an assistant: https://svelte-lean.vercel.app/docs/ai Each link below is the Markdown version of a page; drop `.md` for the HTML page. ## Site - [Browser-native UI for Svelte.](https://svelte-lean.vercel.app/index.md): Svelte Lean is a Svelte-first, browser-native UI ecosystem: semantic HTML first, zero runtime where the browser owns the behavior, small shared behaviors where it does not. - [Proof](https://svelte-lean.vercel.app/proof.md): Live runtime inspector: primitives mounted on the page, registered behaviors, shared listeners counted two ways, loaded behavior modules, measured bundle sizes and browser capabilities. ## Docs - [Introduction](https://svelte-lean.vercel.app/docs.md): What Svelte Lean is and is not, who it is for, the two products, how behavior is shipped, what exists today and where to go next. - [Philosophy](https://svelte-lean.vercel.app/docs/philosophy.md): The core thesis, the three public messages, the seven Lean Rules with their engineering meaning, the mantra, and what the project does not claim. - [Installation](https://svelte-lean.vercel.app/docs/installation.md): Install commands for every package, the three consumption modes with working snippets, the manual register escape hatch, the stylesheets and the build report. - [Browser support](https://svelte-lean.vercel.app/docs/browser-support.md): The versioned support policy, the Baseline features each native path depends on, what happens outside the target, and the opt-in compatibility model. - [Compatibility](https://svelte-lean.vercel.app/docs/compatibility.md): The version matrix of Svelte, SvelteKit, Vite, Node and the toolchain the packages are tested with, read from the repository files; the JavaScript output target and the deprecation rule. - [Markdown for AI](https://svelte-lean.vercel.app/docs/ai.md): Every page as Markdown for assistants: the llms.txt index, llms-full.txt with every page in one file, the .md version of each page, and a prompt to start a migration. - [Native-first](https://svelte-lean.vercel.app/docs/architecture/native-first.md): Why the platform owns behavior first, which native features are relied on, and what "Svelte Lean owns nothing" means. - [Runtime tiers](https://svelte-lean.vercel.app/docs/architecture/runtime-tiers.md): The four runtime tiers with their budgets, how a primitive declares one, the classification of every primitive, and the ADR 0003 targets next to the measured values. - [DOM protocol](https://svelte-lean.vercel.app/docs/architecture/dom-protocol.md): The data-slean attributes, one attribute per option, platform pseudo-classes and ARIA state, slean:* events, the static-marker requirement and the deprecation rule. - [Build-time discovery](https://svelte-lean.vercel.app/docs/architecture/build-time-discovery.md): How @svelte-lean/vite scans static markers, injects register modules, writes the manifest and build report, and why there is no MutationObserver. - [Distribution](https://svelte-lean.vercel.app/docs/distribution.md): Take what you need, ship what you use: the three consumption modes, subpath exports, the five shipping invariants with their tests, the manifest and every escape hatch. - [Accessibility policy](https://svelte-lean.vercel.app/docs/architecture/accessibility.md): Accessibility as a runtime contract: APG conformance, what every contract specifies, focus and portal policy, the test layers and the test matrix with its gaps. - [What we do not do](https://svelte-lean.vercel.app/docs/what-we-dont-do.md): The architecture explained by its omissions: no JavaScript dialog, no listener per instance, no MutationObserver, no mirrored state, no Provider, no positioning engine, no forced virtualization, no hidden cost. - [Trade-offs](https://svelte-lean.vercel.app/docs/trade-offs.md): The costs of the native-first architecture: browser focus, a verbose protocol, author ids, complex widgets, build tooling, browser-defined semantics, and the preview status. - [Compared with plain HTML](https://svelte-lean.vercel.app/docs/why-not-html.md): For native primitives plain HTML is sometimes enough; what the package adds at Tier 0 and at Tier 1, with the measured cost of each. - [Compared with Bits UI](https://svelte-lean.vercel.app/docs/why-not-bits-ui.md): A component model and a platform-first DOM protocol compared without claims about the other library; when to choose which, and how they coexist. - [Compared with shadcn-svelte](https://svelte-lean.vercel.app/docs/why-not-shadcn-svelte.md): Source-owned components and a native-first runtime architecture are different layers; where the ownership line is and how they coexist. - [Compared with raw Svelte](https://svelte-lean.vercel.app/docs/why-not-raw-svelte.md): 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. - [From shadcn-svelte and Bits UI](https://svelte-lean.vercel.app/docs/migration/shadcn-svelte.md): Move an application from shadcn-svelte or Bits UI to native elements with Svelte Lean attributes: what changes, a component map with tiers, before and after examples and a checklist per screen. ## Primitives - [Primitives](https://svelte-lean.vercel.app/primitives.md): Every primitive with its tier, measured behavior JavaScript, platform features and status, and how a primitive page is organised. - [Button](https://svelte-lean.vercel.app/primitives/button.md): Native ``` | Part | Tier | Who does the work | Behavior JavaScript | | --- | --- | --- | --- | | [Dialog](https://svelte-lean.vercel.app/primitives/dialog.md) | 0 | The browser: `` and `command` give the top layer, Escape and focus return. | 0 B brotli | | [Hint](https://svelte-lean.vercel.app/primitives/hint.md) | 1 | One shared behavior, added by the plugin because the file contains `data-slean-hint`. | 2687 B brotli | No component imports, no wrappers, no ids generated at hydration: the markup is the API, and it renders the same on the server. ## What a page ships - Native primitives only (button, dialog, popover): 0 B of Svelte Lean JavaScript in the production fixture. - A page with tabs: the tabs behavior and the shared kernel, 1839 B brotli, for one tab list or a thousand. How this is shipped and verified: [Distribution](https://svelte-lean.vercel.app/docs/distribution.md) and [Performance](https://svelte-lean.vercel.app/performance.md). ## Packages | Package | Role | | --- | --- | | [`@svelte-lean/primitives`](https://svelte-lean.vercel.app/primitives.md) | Contracts and types for all 50 primitives; 17 Tier 1 and 6 Tier 2 behaviors as side-effect register modules | | [`@svelte-lean/table`](https://svelte-lean.vercel.app/table.md) | \, createTable(), optional modules | | [`@svelte-lean/styles`](https://svelte-lean.vercel.app/styling.md) | Optional CSS: tokens, base, one file per primitive | | [`@svelte-lean/vite`](https://svelte-lean.vercel.app/docs/architecture/build-time-discovery.md) | Build-time behavior discovery, manifest, build report | | [`@svelte-lean/core`](https://svelte-lean.vercel.app/docs/architecture/dom-protocol.md) | Behavior registry, shared event router, DOM and keyboard helpers, diagnostics | Version `0.17.4`, a preview; the release stage is recorded in docs/release.md. Not yet available: Firefox and WebKit test runs, screen-reader tests, a command-line tool. Every public claim and its evidence: claim registry. ## Next steps - [Primitives](https://svelte-lean.vercel.app/primitives.md) Every primitive, live, with its cost. - [Table](https://svelte-lean.vercel.app/table.md) DataTable from a plain table to a data grid. - [Styling](https://svelte-lean.vercel.app/styling.md) Tokens, themes, Tailwind, owned CSS. - [Performance](https://svelte-lean.vercel.app/performance.md) What is measured and how. - [Installation](https://svelte-lean.vercel.app/docs/installation.md) Every package and option. - [Philosophy](https://svelte-lean.vercel.app/docs/philosophy.md) Why the platform comes first. --- # Philosophy > The core thesis, the three public messages, the seven Lean Rules with their engineering meaning, the mantra, and what the project does not claim. Page: https://svelte-lean.vercel.app/docs/philosophy · Markdown: https://svelte-lean.vercel.app/docs/philosophy.md · Index: https://svelte-lean.vercel.app/llms.txt Native when possible, tiny behavior when necessary: the browser owns what it already understands, and Svelte Lean adds the smallest runtime only where the platform is incomplete. ## The thesis - **Architecture.** Ask what the browser already does before writing JavaScript: ``, popover, invoker commands, `
`, form controls, anchor positioning (ADR 0001, [Native-first](https://svelte-lean.vercel.app/docs/architecture/native-first.md)). - **Runtime.** It grows with the interaction: a dialog needs no behavior, tabs a shared one, an owned [select](https://svelte-lean.vercel.app/primitives/select.md) a lazy controller. Every primitive declares its [runtime tier](https://svelte-lean.vercel.app/docs/architecture/runtime-tiers.md). - **Distribution.** Installing is not shipping. Native-only markup ships 0 B of behavior JavaScript; a page with tabs ships 1839 B brotli including the kernel ([Distribution](https://svelte-lean.vercel.app/docs/distribution.md)). - **Not zero JavaScript.** A select with search needs real behavior code; the goal is the minimum necessary runtime. ## The Lean Rules Seven rules, in order of precedence, from the architecture contract. | Rule | In practice | | --- | --- | | Native before custom | A modal is a `` opened by `command="show-modal"`, not a controller with a focus trap and a portal. | | Static before reactive | Options are static attributes on the root; nothing runs for a root until its first interaction. | | DOM before duplicated state | `open`, `checked`, `aria-selected`, `tabindex`, `hidden` are the state; no mirror object. | | Shared before per-instance | One listener per event type for every root: a thousand tabs roots keep one `click` and one `keydown`, asserted by a release-gate test. | | Lazy before eager | Roots are found on their first event; a Tier 2 controller is created then and released through an `AbortController`. | | Compile before runtime | The build reads the markup and adds only the registration modules it uses. | | Measure before optimize | Every number comes from a script in the repository; a missing one reads "not measured". | The playbook adds: correctness before cleverness, accessibility before byte shaving, measure before claiming. In one line: if the browser owns the behavior, Svelte Lean owns nothing. ## What it is not - Not a Tailwind component collection: no package depends on a CSS framework. - Not a Radix port or a Headless UI clone: behavior is not a component instance with its own listeners and state. - Not a Web Components library: the markup is plain HTML that Svelte renders and hydrates. - Not a claim that Svelte is slow: the gain is the absence of abstractions the platform makes unnecessary. - Not anti-JavaScript, and not a claim that the browser solves every widget. ## Who it is for - Teams that write markup and want pages to work before hydration and without JavaScript. - Svelte 5 and SvelteKit applications that render on the server: ids are authored, nothing is generated at hydration. - Projects that want to own their CSS and depend only on a maintained behavior engine. - A poorer fit for teams that want a component API for every interaction, or a widget the platform does not cover in their browser target ([Trade-offs](https://svelte-lean.vercel.app/docs/trade-offs.md)). ## What is not claimed - That small bundles are the goal: bytes are one cost among listeners, allocations, layout reads and hydration, and accessibility is never traded for them. - Anything about another library beyond the measured cases on [Compared with Bits UI](https://svelte-lean.vercel.app/docs/why-not-bits-ui.md#measured), which follow the [methodology](https://svelte-lean.vercel.app/performance/methodology.md). - Anything unverified: the claim registry records the evidence of each public statement. See also [What we do not do](https://svelte-lean.vercel.app/docs/what-we-dont-do.md) and [Trade-offs](https://svelte-lean.vercel.app/docs/trade-offs.md). --- # 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

Settings

…
``` 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
…
``` `+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
{ // 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(); }} > …
``` 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 /// 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 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. --- # Browser support > The versioned support policy, the Baseline features each native path depends on, what happens outside the target, and the opt-in compatibility model. Page: https://svelte-lean.vercel.app/docs/browser-support · Markdown: https://svelte-lean.vercel.app/docs/browser-support.md · Index: https://svelte-lean.vercel.app/llms.txt Each native path names the platform features it uses, their Baseline status and what happens without them; the packages ship no polyfills. ## Policy - The browser target is the support of each platform feature, not a version list. - Native paths use the feature directly; no fallback logic is mixed in. - Compatibility code is opt-in or target-selected, never bundled silently into a native path (shipping invariant E of ADR 0005). No compatibility module exists today. - No browser-name branching and, today, no feature detection in the packages: the stylesheets rely on CSS fallback behavior. - The Vite plugin has no `target` option; the blueprint names one, and it is not built. - These statements describe what is tested; they become a versioned promise from release stage 4. ## Feature table Baseline status as each contract documents it on 2026-09-27. | Feature (native path) | Used by | Baseline status, documented in the contract as of 2026-09-27 | Compatibility path | Without support | | --- | --- | --- | --- | --- | | `, showModal(), the top layer, ::backdrop, page inertness` | Dialog | Widely available (newly available in 2022) | none exists; the package does not polyfill | No fallback is documented; the element is the base of the primitive | | `Invoker commands (command, commandfor)` | Dialog and Popover invokers, Button | Newly available, not yet widely: Chrome 135, Safari 26, Firefox 144, shipped during 2025 | None exists; an opt-in module is named as an option in ADR 0005 | Two calls in application code (showModal(), close()); without them the dialog stays closed | | `Popover API (popover, popovertarget, popovertargetaction, :popover-open, ToggleEvent)` | Popover, Menu (and its context menu), Menubar, Select, Autocomplete, Hint, Hover card, Toast, Date picker, the mobile navigation of this site | Newly available since April 2024: Chrome 114, Safari 17, Firefox 125 | none exists; the package does not polyfill | The attribute is ignored and the content renders inline; a menu loses its top layer and light dismiss; the select and autocomplete popups use a hidden-toggled popup instead | | `showPopover({ source })` | Select and autocomplete popups (the trigger as the implicit anchor and invoker), Hint | Not Baseline: Chrome 133, Safari 26 | none exists; the package does not polyfill | Passed as an option that browsers without it ignore; popover="manual" needs no invoker relationship | | `CSS anchor positioning (position-area, position-try-fallbacks)` | Popover, Menu, Menubar and Hover card placement in the stylesheet | Newly available since January 2026: Chrome 125, Safari 26, Firefox 147 | The anchor fallback of @svelte-lean/primitives: the menu, menubar and hover card behaviors load it with import() only where position-area is unsupported (Safari before 26); a plain popover opts in with @svelte-lean/primitives/popover/anchor/register | The open popup is placed next to its invoker with fixed coordinates: the same side, alignment and flip as the stylesheet, clamped into the viewport, its height capped to the room it has, following scrolling and resizing while open. With no script: centered in the top layer | | `CSS anchor positioning (anchor-name, anchor-scope, position-anchor, position-area, anchor-size())` | Select, autocomplete and date picker popup placement in the stylesheet | Newly available since January 2026: Chrome 125 (anchor-scope 131), Safari 26, Firefox 147 | The anchor fallback of @svelte-lean/primitives, loaded with import() by the behavior only where position-area is unsupported | A hidden-toggled popup is positioned under the root with absolute positioning; a popover popup is placed under the field (at least as wide, flipped above when the bottom has no room, its height capped) | | `KeyboardEvent.isComposing` | Autocomplete and select search (keys during an IME composition are ignored) | Widely available; keyCode 229 covers browsers that report the state late | none exists; the package does not polyfill | No fallback is documented | | `AbortSignal on addEventListener, Element.isConnected` | Select and autocomplete session listeners (Tier 2 scoped controllers) | Widely available | none exists; the package does not polyfill | No fallback is documented | | `
, , the toggle event` | Accordion, Nav groups | Widely available | none exists; the package does not polyfill | No fallback is documented; the element is the base of the primitive | | `name on
for exclusive groups` | Accordion items and Nav groups that share a name | Newly available since September 2024: Chrome 120, Safari 17.2, Firefox 130 | none exists; the package does not polyfill | Siblings stay open independently | | `::details-content, interpolate-size` | Accordion and Nav group open transitions in the stylesheet | Used only where supported; nothing depends on them | none exists; the package does not polyfill | No transition | | `appearance: none on checkboxes, :checked, :indeterminate` | Checkbox and its switch look | Widely available | none exists; the package does not polyfill | No fallback is documented | | `:user-invalid` | Validation styles of Checkbox, Input (every type), Select, Autocomplete, Radio group and Field | Baseline 2023 | none exists; the package does not polyfill | Native validation still works; the invalid styling does not apply | | `role="switch" on a checkbox (ARIA 1.2)` | Checkbox, the switch look | Mapped by current screen readers | none exists; the package does not polyfill | Announced as a checkbox where the role is not mapped | | `

Settings

``` Branch on the feature, not the browser name: ```ts // Feature detection before user-agent detection: what application code may branch on. const commands = 'commandForElement' in HTMLButtonElement.prototype; const popover = 'popover' in HTMLElement.prototype; const anchors = CSS.supports('position-area: bottom'); ``` Without JavaScript, behaviors keep their server-rendered state: | Primitive | Without the runtime | | --- | --- | | Tabs | The selected panel shows and cannot switch; render every panel without `hidden` for a stacked layout. | | Menu | Opens as a plain popover; link items work; movement is Tab-based. | | Listbox | Keeps its authored selection. | | Select (owned) | The native select is shown and submits with the form. | | Autocomplete (owned) | The input with its ``: the browser shows the suggestions, the form submits what was typed. | ## Compatibility modules None exists. ADR 0005 names an opt-in invoker-commands module and the blueprint a positioning module; neither is planned. One would be a separate entry point, with a fixture asserting that the native path excludes it (invariant E). ## Testing - Chromium only: the local Google Chrome (version recorded in the audit and the benchmark file) and Playwright's Chromium in CI. - Native contracts run with page JavaScript disabled in native.spec.ts. - Firefox and WebKit run nightly; no run is recorded yet. A recorded run is a stage 2 requirement in docs/release.md. No cross-browser claim is made until then. ## Contract excerpts The "Platform features and Baseline" section of every contract, read from the file at build time. accordion - `
`, `` and the `toggle` event: Baseline widely available. - `name` for exclusive groups: Baseline newly available since September 2024 (Chrome 120, Safari 17.2, Firefox 130). Elsewhere every item opens on its own. - Find-in-page opens a closed item that contains the match: Chrome 97, Firefox 139, Safari 26.2. In an exclusive group this closes the item that was open. - `::details-content`: Baseline newly available since September 2025 (Chrome 131, Safari 18.4, Firefox 143). The styles pad the content's block end through it; without it the open item carries that padding. `interpolate-size: allow-keywords`, which lets the styles animate the height to `auto`, is in Chrome 129 and not in Firefox or Safari; there the content appears without a transition. - `:has()` (Baseline 2023) gives the last item of a joined list its corners; without it the joined corners are square. alert - `role="status"` and `role="alert"` are ARIA 1.1 live region roles, exposed by every current browser. When and how a message is spoken differs between screen readers; the rules above are the ones that hold across them. - `color-mix()` mixes the borders from the color; it is Baseline 2023. Without it the standard alert has no border. - `:empty` (the empty root and the drawn icon) is widely available. autocomplete - `` and ``: the `list` attribute is Baseline widely available; how the native list matches and shows options differs between browsers, which is why the owned form exists. - Popover API: Baseline newly available since April 2024. `showPopover({ source })`: Chrome 133 and Safari 26, ignored elsewhere. - CSS anchor positioning (`control.css`): Baseline newly available since January 2026. In an engine with anchor positioning the popup takes its content height, is capped to the room in its position area and scrolls when capped. In an older engine (Safari before 26) the behavior places a popover popup under the field, at least as wide, capped and flipped above when the bottom has no room (the anchor fallback, loaded with `import()` only there); a hidden popup sits under the field. - `KeyboardEvent.isComposing`, `AbortSignal` on `addEventListener`: Baseline widely available. - ARIA 1.2 combobox, `aria-activedescendant`, `listbox`, `option`: widely supported. avatar - `` with `alt`, `width`, `height` and `loading="lazy"`: Baseline widely available. - CSS grid, which stacks the image and the initials in one cell: Baseline widely available. badge - ``, text and ` ``` ## Usage Install `@svelte-lean/primitives` and `@svelte-lean/styles`. With `@svelte-lean/vite` the behavior registers itself for every `data-slean="input"`; without it, import the register module once. ```bash npm install @svelte-lean/primitives ``` `+layout.svelte or any client module` ```ts import '@svelte-lean/primitives/input/register'; ``` `stylesheets` ```ts import '@svelte-lean/styles/input.css'; ``` Put `data-slean="input"` on an `` of any type, or on a `

{max - bio.length} characters left.

``` The application writes the count into the description; maxlength stops the typing. ### Disabled and read-only ```svelte

Set when it was created.

``` ## Helper or plain markup Two ways to write the same markup: with `fieldParts()` or by hand. Both render the same HTML, which a test of this site compares; use either. `With fieldParts()` ```svelte

Letters, digits and hyphens.

This username is taken.

``` `Plain markup` ```html

Letters, digits and hyphens.

This username is taken.

``` ### What the helper does - One id in, every reference out: `fieldParts('username', { description: true, error: true })` writes `username-description` and `username-error` and lists both, in that order, in the control’s `aria-describedby`. - Strings only: no handler, no state, no instance. The server and the browser render the same attributes, so hydration has nothing to patch. - `error: true` adds `aria-invalid="true"` with the reference, so the state and the message come and go together; `-error` is also the id the validation module uses. ### Points to watch - The id is the control’s own id, not a root id: spread `f.control` on the `` or `, {#if sent}

Sent: {sent}

{/if} ``` The checked radio is submitted under its name; required asks for a value first. ### Disabled ```svelte
Rated by the team {#each [1, 2, 3, 4, 5] as value (value)} {/each}
``` For an average that is not a whole number, show text or a Meter instead. ## Usage Plain markup with a stylesheet: install `@svelte-lean/styles`. No JavaScript runs. ```bash npm install @svelte-lean/styles ``` `stylesheets` ```ts import '@svelte-lean/styles/rating.css'; ``` Put `data-slean="rating"` on a `
` with a ``, and one `