Docs Architecture
Native-first
If HTML, CSS or the browser already provides a behavior, Svelte Lean uses it and owns nothing. A primitive must justify why it is not in a lower runtime tier.
On this page
The decision
Most UI libraries model every interaction as a JavaScript component with its own lifecycle,
state, handlers and context. Browsers now own a large part of that interaction model: <dialog> with invoker commands, the Popover API with its top layer and light
dismiss, <details>, native form controls, CSS anchor positioning.
Re-implementing them costs bytes, main-thread time, accessibility risk and a dependence on
hydration.
ADR 0001 therefore fixes the order: the platform first, and a primitive is JavaScript only when the platform cannot provide the interaction. 40 of the 60 primitives are native today and ship no runtime; the package contributes styles, contract types, documentation and development validation for them. The production fixture that renders button, dialog and popover markup emits a Svelte Lean chunk of 0 B.
Platform features relied on
| Feature | Primitive | What the browser provides |
|---|---|---|
<dialog> with command="show-modal" and command="close" | Dialog | Top layer, page inertness, Escape, focus return to the invoker, the backdrop |
popover with popovertarget | Popover, Menu | Top layer, light dismiss, one auto popover open at a time, aria-expanded on the invoker, the implicit anchor |
Invoker commands (command, commandfor) | Dialog and Popover invokers | Opening and closing without an author-written click handler |
<details> with name | Disclosure | The open state, Enter and Space on the summary, exclusive groups |
Native form controls | Button, Checkbox, Switch, Radio group | Focus, activation, checked state, arrow keys in a radio group, form participation |
CSS anchor positioning (position-area) | Popover and Menu placement | Placement against the invoker, with fallbacks, in the stylesheet |
CSS state selectors (:open, :popover-open, :checked, :disabled, :focus-visible) | Every stylesheet | State for styling without a mirrored attribute |
Each feature's Baseline status and the behavior where it is missing are on Browser support.
Ownership
"Svelte Lean owns nothing" is precise. For a modal dialog the division is:
| Svelte Lean owns | The browser owns |
|---|---|
The stylesheet (dialog.css), applied through the protocol attributes | Open and close, through showModal() and the invoker commands |
| The contract: semantics, parts, keyboard, focus, SSR and JS-disabled behavior | The modal top layer, the backdrop and page inertness |
| Development validation of the markup | Dialog semantics, Escape, focus return, Tab staying inside |
Types (@svelte-lean/primitives/dialog) | Form participation through method="dialog" |
The dialog contract lists what the package does not do: no focus trap, no portal, no z-index
manager, no scroll lock, no positioning engine, no open mirror. The runtime block
on the Dialog page reads the same facts from the contract and
the measurement file: tier 0, shared listeners
none, native base <dialog> + command/commandfor.
Examples
Two native primitives, rendered with @svelte-lean/styles. The HTML and Svelte tabs
show the same markup, because a Tier 0 primitive has nothing to import; the Svelte file adds the
stylesheet import only.
<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>
<p data-slean-part="description">
Changes apply to this device.
</p>
</header>
<div data-slean-part="body">…</div>
<footer data-slean-part="footer">
<button type="button" commandfor="settings"
command="close">Close</button>
</footer>
</dialog><script lang="ts">
// Nothing to import: the browser owns the dialog.
// The styles are optional.
import '@svelte-lean/styles/dialog.css';
</script>
<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>
<p data-slean-part="description">
Changes apply to this device.
</p>
</header>
<div data-slean-part="body">…</div>
<footer data-slean-part="footer">
<button type="button" commandfor="settings"
command="close">Close</button>
</footer>
</dialog>Signed in as Amara Okafor.
<button type="button" popovertarget="account">Account</button>
<div id="account" popover data-slean="popover" data-slean-side="bottom" data-slean-align="start">
<p>Signed in as Amara Okafor.</p>
</div><script lang="ts">
// Nothing to import: the Popover API owns the top layer and light dismiss.
import '@svelte-lean/styles/popover.css';
</script>
<button type="button" popovertarget="account">Account</button>
<div id="account" popover data-slean="popover" data-slean-side="bottom" data-slean-align="start">
<p>Signed in as Amara Okafor.</p>
</div>Consequences
- Browser support is explicit and versioned: native paths depend on Baseline features, and each contract names them with the fallback.
- Native primitives are interactive before hydration and when client JavaScript fails; the playground runs the native contracts with page JavaScript disabled.
- Documentation presents the native, copy-only path first and never hides it behind a wrapper component. Every primitive page opens with plain markup.
- Native behavior has browser-defined semantics. That is usually the point, and sometimes a limit on customization; the trade-offs page says where.
Revisit when
A required behavior cannot be met by a Baseline platform feature for the documented browser target. That is also the moment a primitive moves up a tier, with the reason written in its contract.
Source
- ADR 0001, Native before JavaScript
- Dialog contract and Popover contract
- native.spec.ts: the browser tests, including the runs with JavaScript disabled
- fixtures/native-only: the production build that asserts no behavior runtime