Primitives Forms
Checkbox group
Native checkboxes with a select-all box that checks them all and shows whether all, some or none are checked.
Markdown version of this page, for assistants (Markdown for AI)
On this page
Value: mentions
Code
<script lang="ts">
// data-slean="checkbox-group" registers the behavior with @svelte-lean/vite.
import '@svelte-lean/styles/checkbox.css';
import '@svelte-lean/styles/checkbox-group.css';
const all = ['mentions', 'replies', 'digest'];
let notify = $state(['mentions']);
</script>
<fieldset data-slean="checkbox-group">
<legend>Notifications</legend>
<label>
<!-- Server-render the state of the all box; indeterminate is a property Svelte sets. -->
<input
type="checkbox"
data-slean="checkbox"
data-slean-part="all"
checked={notify.length === all.length}
indeterminate={notify.length > 0 && notify.length < all.length}
/>
All notifications
</label>
<label>
<input type="checkbox" name="notify" value="mentions" data-slean="checkbox" bind:group={notify} />
Mentions
</label>
<label>
<input type="checkbox" name="notify" value="replies" data-slean="checkbox" bind:group={notify} />
Replies
</label>
<label>
<input type="checkbox" name="notify" value="digest" data-slean="checkbox" bind:group={notify} />
Weekly digest
</label>
</fieldset>
<p>Value: {notify.join(', ') || 'none'}</p>Examples
Nested groups
Code
<script lang="ts">
import '@svelte-lean/styles/checkbox.css';
import '@svelte-lean/styles/checkbox-group.css';
</script>
<!-- A nested group is a member of the outer one through its own all box. -->
<fieldset data-slean="checkbox-group">
<legend>Permissions</legend>
<label><input type="checkbox" data-slean="checkbox" data-slean-part="all" /> Everything</label>
<label><input type="checkbox" name="perm" value="read" data-slean="checkbox" /> Read</label>
<fieldset data-slean="checkbox-group">
<legend>Write</legend>
<label>
<input type="checkbox" data-slean="checkbox" data-slean-part="all" /> All writes
</label>
<label><input type="checkbox" name="perm" value="edit" data-slean="checkbox" /> Edit</label>
<label><input type="checkbox" name="perm" value="delete" data-slean="checkbox" /> Delete</label>
</fieldset>
</fieldset>Disabled members, one row
Code
<script lang="ts">
import '@svelte-lean/styles/checkbox.css';
import '@svelte-lean/styles/checkbox-group.css';
</script>
<!-- A disabled member keeps its state: the all box skips it and leaves it out of its own. -->
<fieldset data-slean="checkbox-group" data-slean-orientation="horizontal">
<legend>Exports</legend>
<label><input type="checkbox" data-slean="checkbox" data-slean-part="all" /> All formats</label>
<label><input type="checkbox" name="format" value="csv" data-slean="checkbox" /> CSV</label>
<label><input type="checkbox" name="format" value="xlsx" data-slean="checkbox" /> Excel</label>
<label>
<input type="checkbox" name="format" value="pdf" data-slean="checkbox" disabled />
PDF (upgrade)
</label>
</fieldset>Usage
Install @svelte-lean/primitives and @svelte-lean/styles. With @svelte-lean/vite the behavior registers itself for every data-slean="checkbox-group"; without it, import the register module once.
npm install @svelte-lean/primitivespnpm add @svelte-lean/primitivesyarn add @svelte-lean/primitivesbun add @svelte-lean/primitivesimport '@svelte-lean/primitives/checkbox-group/register';import '@svelte-lean/styles/checkbox.css';
import '@svelte-lean/styles/checkbox-group.css';Put data-slean="checkbox-group" on a <fieldset> with a <legend>, and mark the select-all checkbox data-slean-part="all". Leave the name off it: it is not a form value. Draw the checkboxes with data-slean="checkbox" and import checkbox.css.
Render the all box’s state from the template when you can (checked when every member is, Svelte’s indeterminate={…} for some); otherwise the behavior sets it on the first focus in the group. Nest a group inside another for a tree of permissions.
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <fieldset data-slean="checkbox-group"> with a <legend> | – | yes | data-slean-orientation="horizontal" puts the members on one row. |
| all | <input type="checkbox"> without a name | all | yes | Checks or clears every enabled member; shows their state. Not a form value. |
| members | <input type="checkbox" name="…" value="…"> | – | yes | The other checkboxes of the group; a nested group counts through its own all box. |
Keyboard
| Key | When | Result |
|---|---|---|
| Space | focus on a checkbox | Toggles it (native) |
| Tab/Shift+Tab | in the group | Moves between the checkboxes |
Styling
checkbox-group.css lays out the fieldset (the members indented under the all box, or one row with data-slean-orientation="horizontal"); the boxes are checkbox.css. The tokens it reads:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-space-2 | calc(var(--slean-spacing) * 2) | gap, margin-inline-start |
--slean-space-1 | calc(var(--slean-spacing) * 1) | margin-block-end |
--slean-fg | var(--slean-neutral-12) | color |
--slean-text-sm | 0.875rem | font-size |
--slean-font-weight-medium | 500 | font-weight |
--slean-space-4 | calc(var(--slean-spacing) * 4) | column-gap |
--slean-fg-muted | var(--slean-neutral-11) | color |
State selectors the stylesheet targets, all from the platform or ARIA: :disabled.
Variant attributes: data-slean-orientation (horizontal).
Under the hood
Runtime and size
- Tier
- 1 · Delegated micro-behavior
- Behavior JS
- 1456 B brotli · 1638 B gzip ·
packages/· methodprimitives/ artifacts/ size.json - Platform features
<fieldset>,<input type="checkbox">,indeterminate,:indeterminate- Shared listeners
- change, focusin, reset
- Per-instance listeners
- none
- Lazy state
- none
- Native base
<fieldset> + <input type="checkbox">, indeterminate
Tier 1: shared change, focusin and reset listeners on the document; no per-group listener and no state. Without the module the checkboxes still work and submit; only the all box stops following.
Why this implementation
A select-all box is the one thing a set of native checkboxes cannot do alone: it has to set the others and show their sum, including the mixed state, which HTML cannot write in markup. Every admin table and settings page has one.
The group stays native: every checkbox keeps its value, Space, the label click and the form value, and bind:group keeps working. One shared change listener, written once for every group on the page, checks the enabled members from the all box (dispatching input and change on each, as a click would) and sets the all box from the members. There is no state to keep: the checkboxes are the state.
The browser owns
- every checkbox: its checked state, Space, the label click, the form value
- the mixed state of the all box (the indeterminate property, exposed as mixed)
- the group name from the fieldset’s legend
Svelte Lean owns
- checking or clearing every enabled member from the all box, with input and change on each
- the all box’s checked, mixed or unchecked state from the members, nested groups included
- checkbox-group.css: the fieldset layout and the indent under the all box
Accessibility
- The legend names the group; every checkbox keeps its own label.
- The mixed state is the native
indeterminateproperty, which screen readers announce as mixed or partially checked; noaria-checkedis written. - The behavior never moves focus. Disabled members are left as they are and are not counted.
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
<fieldset>, <legend>, the indeterminate property, :indeterminate | Widely available | Not applicable within the support policy |
Without JavaScript
Every checkbox works and submits. The all box toggles only itself and submits nothing (it has no name).
Server rendering and hydration
Every checkbox renders its checked attribute; render the all box checked when every member is. The mixed state needs script: Svelte sets it during hydration, or the behavior on the first focus.
Nothing is attached per group. The first change or focus in a group is handled by the document listeners.
Compatibility
<fieldset>, <legend> and the indeterminate property are widely available.
Tests and source
packages/primitives/tests/checkbox-group.test.tsVitest: all and members, disabled members, nested groups, focus and reset, 1000 groupsapps/playground/tests/primitives/fields.spec.tsPlaywright, with page JavaScript enabled and disabled, and axepackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)
packages/primitives/src/checkbox-group/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/checkbox-group/contract.tstyped constants: name, tier, base, parts, options, eventspackages/primitives/src/checkbox-group/behavior.tsthe behavior definition and its helperspackages/primitives/src/checkbox-group/register.tsthe registration modulepackages/styles/css/checkbox-group.cssthe optional stylesheet