sveltelean
Versionv0.2.0 GitHub

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.

How a primitive is assigned its runtime tierCan the browser do it? Yes: native, Tier 0, no Svelte Lean JavaScript. No: is one shared listener per event type enough? Yes: shared behavior, Tier 1. No: lazy scoped controller, Tier 2, with state created on first interaction.Can the browser do it?yesnoNative · Tier 0no Svelte Lean JavaScriptIs one shared listener perevent type enough?yesnoShared behavior · Tier 1one listener per event typeTier 2lazy scopedcontroller How a primitive is assigned its runtime tierCan the browser do it? Yes: native, Tier 0, no Svelte Lean JavaScript. No: is one shared listener per event type enough? Yes: shared behavior, Tier 1. No: lazy scoped controller, Tier 2, with state created on first interaction.Can the browser do it?yesnoNative · Tier 0no Svelte Lean JavaScriptIs one shared listener perevent type enough?yesnoShared behavior · Tier 1one listener per event typeLazy scoped controller · Tier 2state created on first interaction
Native before JavaScript. Every primitive declares its tier in its contract; the budgets are on Runtime tiers.

Platform features relied on

FeaturePrimitiveWhat the browser provides
<dialog> with command="show-modal" and command="close"DialogTop layer, page inertness, Escape, focus return to the invoker, the backdrop
popover with popovertargetPopover, MenuTop 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 invokersOpening and closing without an author-written click handler
<details> with nameDisclosureThe open state, Enter and Space on the summary, exclusive groups
Native form controlsButton, Checkbox, Switch, Radio groupFocus, activation, checked state, arrow keys in a radio group, form participation
CSS anchor positioning (position-area)Popover and Menu placementPlacement against the invoker, with fallbacks, in the stylesheet
CSS state selectors (:open, :popover-open, :checked, :disabled, :focus-visible)Every stylesheetState 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 ownsThe browser owns
The stylesheet (dialog.css), applied through the protocol attributesOpen and close, through showModal() and the invoker commands
The contract: semantics, parts, keyboard, focus, SSR and JS-disabled behaviorThe modal top layer, the backdrop and page inertness
Development validation of the markupDialog 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.

Dialog · Tier 0

Settings

Changes apply to this device.

The rest of the page is inert while this dialog is open.

<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>
Open it, press Escape, and watch focus return to the button. The two sources differ only by the stylesheet import; the interaction is the browser's.
Popover · Tier 0

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>
Light dismiss, Escape and the top layer come from the Popover API; the placement below the button is CSS anchor positioning where the browser supports it, and the platform's centered default where it does not.

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