Primitives Forms
Checkbox
A native <input type="checkbox"> styled around the platform control. State is :checked, :indeterminate and :disabled; the package never mirrors it and ships no runtime. Tier 0.
- Tier
- 0 · Native
- Behavior JS
- 0 B brotli · 0 B gzip ·
fixtures/· methodresults.json (native-only) - Platform features
<input type="checkbox">,:checked,:indeterminate,:user-invalid- Shared listeners
- none
- Per-instance listeners
- none
- Lazy state
- none
- Native base
<input type="checkbox">
On this page
Example
<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><script lang="ts">
// Nothing to import: the browser owns the checkbox. The stylesheet is optional.
import '@svelte-lean/styles/checkbox.css';
</script>
<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>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/stylespnpm add @svelte-lean/stylesyarn add @svelte-lean/stylesbun add @svelte-lean/stylesimport '@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
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <input type="checkbox"> | – | yes | data-slean="checkbox" marks it for the styles. State is :checked and :indeterminate. |
| label | <label> wrapping the input, or for and id | – | yes | Related 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>, orforandid. - State is
:checked,:indeterminate,:disabledand, withrequired,:user-invalid. Noaria-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
indeterminateproperty, set from script; it is not an attribute and cannot be server-rendered.
Keyboard
| Key | When | Result |
|---|---|---|
| Space | focus on the checkbox | Toggles (native) |
| Tab/Shift+Tab | anywhere | Moves focus (native) |
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
appearance: none, :checked, :indeterminate | Widely available | Not applicable within the support policy |
:user-invalid | Baseline 2023 | The 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:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-control-border | var(--slean-border-strong) | border |
--slean-radius-sm | 0.375rem | border-radius |
--slean-control-bg | var(--slean-surface) | background |
--slean-accent-fg | oklch(100% 0 0) | color |
--slean-duration-fast | 100ms | transition |
--slean-ease | cubic-bezier(0.2, 0, 0, 1) | transition |
--slean-icon-check | url("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-accent | oklch(54% 0.19 258) | border-color, background |
--slean-icon-minus | url("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-danger | oklch(55% 0.2 25) | border-color |
--slean-control-border-disabled | var(--slean-border) | border-color |
--slean-control-bg-disabled | var(--slean-muted) | background |
--slean-fg-muted | var(--slean-neutral-11) | color |
--slean-space-2 | 0.5rem | gap |
--slean-fg | var(--slean-neutral-12) | color |
--slean-text-sm | 0.875rem | font-size |
--slean-leading | 1.5 | line-height |
--slean-control-border-hover | var(--slean-accent) | border-color |
--slean-control-height-md | 2.25rem | inset |
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.
<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><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
apps/playground/tests/primitives/native.spec.tsPlaywright, with page JavaScript enabled and disabledapps/playground/tests/primitives/a11y.spec.tsaxe on every playground page and on open dialog, popover and menu statespackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)fixtures/native-onlyconsumer build asserting that no behavior runtime ships (invariant A)
Source
packages/primitives/src/checkbox/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/checkbox/contract.tstyped constants: name, tier, base, parts, options, eventspackages/styles/css/checkbox.cssthe optional stylesheet