sveltelean Primitives
Version v0.17.4
Notifications
Notifications

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>
Press the all box, then clear one member: the all box turns mixed and bind:group follows both.

Examples

Nested groups

Permissions
Permissions
Write
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>
Edit one write permission: both all boxes turn mixed. Everything checks the whole tree.

Disabled members, one row

Export formats
Exports
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/primitives
+layout.svelte or any client module
import '@svelte-lean/primitives/checkbox-group/register';
stylesheets
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

PartElementdata-slean-partRequiredNotes
root<fieldset data-slean="checkbox-group"> with a <legend>–yesdata-slean-orientation="horizontal" puts the members on one row.
all<input type="checkbox"> without a nameallyesChecks or clears every enabled member; shows their state. Not a form value.
members<input type="checkbox" name="…" value="…">–yesThe other checkboxes of the group; a nested group counts through its own all box.

Keyboard

KeyWhenResult
Spacefocus on a checkboxToggles it (native)
Tab/Shift+Tabin the groupMoves 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:

TokenDefault (light)Applies to
--slean-space-2calc(var(--slean-spacing) * 2)gap, margin-inline-start
--slean-space-1calc(var(--slean-spacing) * 1)margin-block-end
--slean-fgvar(--slean-neutral-12)color
--slean-text-sm0.875remfont-size
--slean-font-weight-medium500font-weight
--slean-space-4calc(var(--slean-spacing) * 4)column-gap
--slean-fg-mutedvar(--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/primitives/artifacts/size.json · method
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 indeterminate property, which screen readers announce as mixed or partially checked; no aria-checked is written.
  • The behavior never moves focus. Disabled members are left as they are and are not counted.
Platform features
FeatureBaselineOutside the target
<fieldset>, <legend>, the indeterminate property, :indeterminateWidely availableNot 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