Checkbox
Form ControlsLocal guide ↗
import { Checkbox } from "bits-ui" Three states
Bind checked and indeterminate independently; data-state exposes every visual state
indeterminate
<script lang="ts">
import { Checkbox, Label } from 'bits-ui';
import { Check, Minus } from '@lucide/svelte';
let checked = $state(false);
let indeterminate = $state(true);
function setState(next: 'unchecked' | 'indeterminate' | 'checked') {
checked = next === 'checked';
indeterminate = next === 'indeterminate';
}
</script>
<div class="grid gap-5">
<div class="flex items-center gap-3">
<Checkbox.Root
id="tri-state"
bind:checked
bind:indeterminate
class="inline-flex size-6 items-center justify-center rounded-md border border-ink-600 bg-ink-900 text-ink-950 transition data-[state=checked]:border-brand-500 data-[state=checked]:bg-brand-500 data-[state=indeterminate]:border-accent-500 data-[state=indeterminate]:bg-accent-500"
>
{#snippet children({ checked, indeterminate })}
{#if indeterminate}
<Minus class="size-4" strokeWidth={3} />
{:else if checked}
<Check class="size-4" strokeWidth={3} />
{/if}
{/snippet}
</Checkbox.Root>
<Label.Root for="tri-state" class="text-sm font-medium text-ink-50">Review status</Label.Root>
<span class="chip ml-auto capitalize">{indeterminate ? 'indeterminate' : checked ? 'checked' : 'unchecked'}</span>
</div>
<div class="flex flex-wrap gap-2">
<button class="btn btn-sm" onclick={() => setState('unchecked')}>Unchecked</button>
<button class="btn btn-sm" onclick={() => setState('indeterminate')}>Indeterminate</button>
<button class="btn btn-sm" onclick={() => setState('checked')}>Checked</button>
</div>
</div>
Parent and children
A derived parent state controls a Checkbox.Group without duplicating child state
2/3
<script lang="ts">
import { Checkbox, Label } from 'bits-ui';
import { Check, Minus } from '@lucide/svelte';
const projects = [
{ value: 'atlas', label: 'Atlas redesign' },
{ value: 'relay', label: 'Relay integration' },
{ value: 'docs', label: 'Documentation refresh' }
];
let selected = $state<string[]>(['atlas', 'docs']);
const allSelected = $derived(selected.length === projects.length);
const someSelected = $derived(selected.length > 0 && !allSelected);
function selectAll(checked: boolean) {
selected = checked ? projects.map((project) => project.value) : [];
}
</script>
<div class="panel mx-auto max-w-md p-4">
<div class="flex items-center gap-3 border-b border-ink-800 pb-3">
<Checkbox.Root
id="all-projects"
checked={allSelected}
indeterminate={someSelected}
onCheckedChange={selectAll}
class="inline-flex size-6 items-center justify-center rounded-md border border-ink-600 bg-ink-900 text-ink-950 data-[state=checked]:border-brand-500 data-[state=checked]:bg-brand-500 data-[state=indeterminate]:border-accent-500 data-[state=indeterminate]:bg-accent-500"
>
{#snippet children({ checked, indeterminate })}
{#if indeterminate}
<Minus class="size-4" strokeWidth={3} />
{:else if checked}
<Check class="size-4" strokeWidth={3} />
{/if}
{/snippet}
</Checkbox.Root>
<Label.Root for="all-projects" class="font-medium text-ink-50">All projects</Label.Root>
<span class="muted ml-auto text-xs">{selected.length}/{projects.length}</span>
</div>
<Checkbox.Group bind:value={selected} class="mt-3 grid gap-3">
{#each projects as project (project.value)}
<div class="flex items-center gap-3">
<Checkbox.Root
id={`project-${project.value}`}
value={project.value}
class="inline-flex size-5 items-center justify-center rounded border border-ink-600 bg-ink-900 text-ink-950 data-[state=checked]:border-brand-500 data-[state=checked]:bg-brand-500"
>
{#snippet children({ checked })}
{#if checked}<Check class="size-3.5" strokeWidth={3} />{/if}
{/snippet}
</Checkbox.Root>
<Label.Root for={`project-${project.value}`} class="text-sm text-ink-200">
{project.label}
</Label.Root>
</div>
{/each}
</Checkbox.Group>
</div>
Required form value
name creates the hidden input and value controls the submitted payload
<script lang="ts">
import { Checkbox, Label } from 'bits-ui';
import { Check } from '@lucide/svelte';
let accepted = $state(false);
let result = $state('Submit to inspect the form value');
function submit(event: SubmitEvent) {
event.preventDefault();
const data = new FormData(event.currentTarget as HTMLFormElement);
result = `deployment-consent = ${data.get('deployment-consent')}`;
}
</script>
<form class="panel mx-auto grid max-w-md gap-4 p-4" onsubmit={submit}>
<div class="flex items-start gap-3">
<Checkbox.Root
id="deployment-consent"
bind:checked={accepted}
name="deployment-consent"
value="approved"
required
class="mt-0.5 inline-flex size-6 shrink-0 items-center justify-center rounded-md border border-ink-600 bg-ink-900 text-ink-950 data-[state=checked]:border-brand-500 data-[state=checked]:bg-brand-500"
>
{#snippet children({ checked })}
{#if checked}<Check class="size-4" strokeWidth={3} />{/if}
{/snippet}
</Checkbox.Root>
<div>
<Label.Root for="deployment-consent" class="text-sm font-medium text-ink-50">
Approve production deployment
</Label.Root>
<p class="muted mt-1 text-xs">Required before the release can be submitted.</p>
</div>
</div>
<div class="flex flex-wrap items-center gap-3">
<button class="btn btn-primary btn-sm" type="submit">Submit approval</button>
<code class="text-xs text-brand-400">{result}</code>
</div>
</form>
checked and indeterminate are separate bindable booleans. The root reports all three visual states through data-state, while its children snippet receives both values for icon rendering.
Passing name renders a hidden checkbox input; value replaces its default "on" payload, and required is forwarded to that hidden input. A Checkbox.Group propagates its name, required, disabled, and readonly settings to descendant roots and submits one same-named hidden input per selected value.