Checkbox
Single binary or tri-state control on bits-ui Checkbox. Border eases to primary on check, the tick icon pops in without layout shift, and the focus ring sits outside the shape so nothing reflows.
Usage
checkbox.svelte
<script lang="ts">
import { Checkbox } from '$lib/components/ui/checkbox';
import { Label } from '$lib/components/ui/label';
let checked = $state(false);
</script>
<div class="flex items-center gap-2">
<Checkbox id="terms" bind:checked />
<Label for="terms">Accept terms and conditions</Label>
</div>States
checkbox.svelte
<script lang="ts">
import { Checkbox as CheckboxPrimitive } from 'bits-ui';
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
import { Icon } from '$lib/components/ui/icon';
import { animate } from 'motion';
import { springs, prefersReducedMotion } from '$lib/motion/index.js';
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
...restProps
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
let iconWrap = $state<HTMLDivElement | null>(null);
// Spring the indicator on mount whenever the icon node appears.
$effect(() => {
// re-runs when checked or indeterminate flips and a fresh icon mounts
if (!iconWrap) return;
const icon = iconWrap.querySelector('i');
if (!icon || prefersReducedMotion()) return;
animate(icon, { scale: [0.6, 1], opacity: [0, 1] }, springs.snappy);
});
</script>
<CheckboxPrimitive.Root
bind:ref
data-slot="checkbox"
class={cn(
'peer relative flex size-4 shrink-0 items-center justify-center rounded-sm border border-input transition-shadow outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2.5 focus-visible:border-ring focus-visible:ring-(length:--ring-width) focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-(length:--ring-width) aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary dark:data-[state=indeterminate]:bg-primary',
className
)}
bind:checked
bind:indeterminate
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<div
bind:this={iconWrap}
data-slot="checkbox-indicator"
class="grid place-content-center text-current transition-none [&>i]:size-3.5"
>
{#if checked}
<Icon name="check-line" size="0.875rem" />
{:else if indeterminate}
<Icon name="subtract-line" size="0.875rem" />
{/if}
</div>
{/snippet}
</CheckboxPrimitive.Root>
API reference
Inherits bits-ui Checkbox.Root props via spread. Supports indeterminate as a bindable third state alongside checked.
| Prop | Type | Default | Description |
|---|---|---|---|
boolean (bindable) | false | Whether the box is checked. Two-way bindable. | |
boolean (bindable) | false | Shows the dash icon and reports mixed to assistive tech. Setting true clears checked visually. | |
boolean | false | Disables the control and drops opacity to 50%. | |
(checked: boolean) => void | — | Fires whenever the checked state changes. | |
'true' | 'false' | — | Flips the border and focus ring to destructive. Pair with field-error for the text. | |
HTMLButtonElement | null | null | Two-way-bindable element reference. | |
string | — | Merged onto the root via tailwind-merge. |