sveltelean Primitives
Versionv0.2.0 GitHub

Example

Checkboxes
<label>
	<input type="checkbox" name="terms" data-slean="checkbox" />
	I agree to the terms
</label>
<label>
	<input type="checkbox" name="updates" data-slean="checkbox" checked />
	Product updates by email
</label>
<label>
	<input type="checkbox" name="locked" data-slean="checkbox" disabled />
	Locked by an administrator
</label>
Tier 0: the two sources differ only by the stylesheet import. Click the label or press Space on the input; the mark is drawn by the stylesheet on the native :checked state. Everything here works with page JavaScript disabled.

Why this implementation exists

The native checkbox has the checked, indeterminate and disabled states, Space to toggle, the label association, form participation, constraint validation and the accessible role and name. A <div role="checkbox"> re-creates part of that in JavaScript and loses the form. With appearance: none the native control can be drawn any way, so the reason to replace it is gone. Svelte Lean therefore ships a stylesheet and a contract and nothing else; the state is read from the input or from FormData, never from a mirror.

The browser owns

  • the checked, indeterminate and disabled state
  • Space to toggle and the label click
  • form participation, required and the change event
  • the accessible role, name and state

Svelte Lean owns

  • checkbox.css: the drawn mark, the indeterminate bar, the coarse-pointer hit area
  • the contract
  • documentation

Usage

The markup needs no package. Install @svelte-lean/styles for the stylesheet; @svelte-lean/primitives adds the typed contract and nothing at runtime.

npm install @svelte-lean/styles
app.css or +layout.svelte
import '@svelte-lean/styles/tokens.css';
import '@svelte-lean/styles/base.css';
import '@svelte-lean/styles/checkbox.css';

Use a checkbox for a value that is submitted with a form or applied on save. Use a Switch when toggling has an immediate effect. Related checkboxes go in a <fieldset> with a <legend>; a single choice among several is a Radio group.

Anatomy

PartElementdata-slean-partRequiredNotes
root<input type="checkbox">–yesdata-slean="checkbox" marks it for the styles. State is :checked and :indeterminate.
label<label> wrapping the input, or for and id–yesRelated checkboxes go in a <fieldset> with a <legend>.

Runtime profile

The runtime block reads the tier and the events from the contract and the bytes from the native-only consumer fixture: a production Vite build with the Vite plugin whose module graph contains no @svelte-lean/core or @svelte-lean/primitives module. The fixture's markup is button, dialog and popover; the checkbox path is proven the same way by construction: packages/primitives/src/checkbox has no behavior, validate or register module, the plugin maps checkbox to no module, and the playground's bundle spec asserts that the native page, which includes this markup, loads no behavior runtime. Nothing is attached at hydration.

Accessibility contract

  • An <input type="checkbox"> with a label: a wrapping <label>, or for and id.
  • State is :checked, :indeterminate, :disabled and, with required, :user-invalid. No aria-checked; the native state is exposed.
  • disabled: not focusable and not submitted. Related checkboxes are grouped by a <fieldset> with a <legend>.
  • The indeterminate state is the indeterminate property, set from script; it is not an attribute and cannot be server-rendered.

Keyboard

KeyWhenResult
Spacefocus on the checkboxToggles (native)
Tab/Shift+TabanywhereMoves focus (native)

Platform features

FeatureBaselineOutside the target
appearance: none, :checked, :indeterminateWidely availableNot applicable within the support policy
:user-invalidBaseline 2023The invalid border is not drawn; the browser still reports the constraint

Without JavaScript

Fully functional. The playground's native page runs the click, label and Space assertions with page JavaScript disabled. Only the indeterminate state, being a property, needs a script.

Server rendering

The checked attribute renders the initial state and forms work before hydration. Ids are authored when for is used.

Before hydration

The delayed-hydration test clicks a label and asserts the checkbox is checked while every script response is held back. Hydration attaches nothing to an input the package does not bind; an application that uses bind:checked follows Svelte's own hydration rules for bound inputs.

Styling

checkbox.css sets appearance: none and draws the mark with borders on ::before, a bar for :indeterminate, a danger border for :user-invalid, and on coarse pointers an invisible hit area on ::after. Under forced colors it opts out of the automatic adjustment and uses the system colors. The tokens it reads and the states it targets:

TokenDefault (light)Applies to
--slean-control-bordervar(--slean-border-strong)border
--slean-radius-sm0.375remborder-radius
--slean-control-bgvar(--slean-surface)background
--slean-accent-fgoklch(100% 0 0)color
--slean-duration-fast100mstransition
--slean-easecubic-bezier(0.2, 0, 0, 1)transition
--slean-icon-checkurl("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.5l3 3 6-7' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E")mask
--slean-accentoklch(54% 0.19 258)border-color, background
--slean-icon-minusurl("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8h9' stroke='black' stroke-width='1.5' stroke-linecap='round' fill='none'/%3E%3C/svg%3E")mask-image
--slean-dangeroklch(55% 0.2 25)border-color
--slean-control-border-disabledvar(--slean-border)border-color
--slean-control-bg-disabledvar(--slean-muted)background
--slean-fg-mutedvar(--slean-neutral-11)color
--slean-space-20.5remgap
--slean-fgvar(--slean-neutral-12)color
--slean-text-sm0.875remfont-size
--slean-leading1.5line-height
--slean-control-border-hovervar(--slean-accent)border-color
--slean-control-height-md2.25reminset

State selectors the stylesheet targets, all from the platform or ARIA: :checked, :disabled, :hover, :indeterminate, :user-invalid, [aria-invalid="true"].

Controlled integration

The input is the state. Svelte reads and writes it with bind:checked and bind:group, and bind:indeterminate covers the mixed state of a "select all" control. The package mirrors nothing and dispatches no slean:* event for a checkbox; a Svelte adapter is not built and is not needed for this primitive.

preferences.svelte
<script lang="ts">
	// The input is the state. bind:checked reads and writes it; FormData reads it on submit.
	let agreed = $state(false);
	let channels = $state<string[]>(['email']);
</script>

<label>
	<input type="checkbox" name="terms" data-slean="checkbox" bind:checked={agreed} required />
	I agree to the terms
</label>

<fieldset>
	<legend>Notifications</legend>
	<label><input type="checkbox" data-slean="checkbox" bind:group={channels} value="email" /> Email</label>
	<label><input type="checkbox" data-slean="checkbox" bind:group={channels} value="sms" /> SMS</label>
</fieldset>
select-all.svelte
<script lang="ts">
	let rows = $state([true, false, true]);
	const checked = $derived(rows.filter(Boolean).length);
	// indeterminate is a property, not an attribute: it is set from script and cannot be
	// server-rendered. Svelte binds it like checked.
	let mixed = $derived(checked > 0 && checked < rows.length);
	let all = $derived(checked === rows.length);
</script>

<label>
	<input
		type="checkbox"
		data-slean="checkbox"
		bind:indeterminate={mixed}
		checked={all}
		onchange={(event) => (rows = rows.map(() => event.currentTarget.checked))}
	/>
	Select all
</label>

Compatibility notes

appearance: none, :checked and :indeterminate are Baseline widely available; :user-invalid is Baseline 2023 and only affects the invalid border. There is no fallback case for the control itself. The policy is on Browser support.

Testing

Source