Checkbox
checkboxuiStandalone or in-form binary toggle with Svelte 5 runes (no headless dependency — keyboard, tri-state, and group context are hand-rolled). Supports indeterminate state for tri-state lists, sizes, colors, label/hint/error plumbing, and proper keyboard / screen-reader behavior. CheckboxGroup renders option arrays or manages slotted checkboxes with bind:value.
Installation
$pnpm dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/checkbox.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/checkbox.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/checkbox.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/checkbox.jsonhttps://uipkge.dev/r/svelte/checkbox.jsonInstalls to:src/lib/components/ui/checkbox/Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
checkedThe controlled checked state. Bindable (`bind:checked`); falls back to internal state when unbound. | CheckboxChecked | null | — | optional |
valueThe value given as data when submitted with a name. Inside a group, drives membership. | string | — | optional |
idId of the element. Auto-generated when `label` is set without an explicit `id`. | string | — | optional |
sizeSize of the checkbox. | 'sm' | 'md' | 'lg' | md | optional |
colorCustom color for the checked state — matches the Vuetify color system. | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | string | primary | optional |
labelLabel text displayed next to the checkbox. | string | — | optional |
hintHint text shown below the checkbox. | string | — | optional |
errorMessagesError message(s) to display. | string | string[] | — | optional |
errorWhether to show error state. | boolean | false | optional |
readonlyWhen `true`, the checkbox is in readonly state (focusable but not toggleable). | boolean | false | optional |
indeterminateWhen `true`, shows an indeterminate state. | boolean | false | optional |
densityDensity of the checkbox — affects spacing. | 'compact' | 'default' | 'comfortable' | default | optional |
hideIconWhen `true`, the icon is hidden. | boolean | false | optional |
loadingLoading state — shows a spinner. | boolean | false | optional |
rippleRipple effect on click (accepted for API parity; no visual effect). | boolean | — | optional |
checkedIconCustom icon to show when checked. | Snippet | — | optional |
indeterminateIconCustom icon to show when indeterminate. | Snippet | — | optional |
labelPositionLabel position — before or after the checkbox. | 'before' | 'after' | after | optional |
flatWhether the checkbox appears flat (no elevation). | boolean | false | optional |
inlineInline text style. | boolean | false | optional |
nameName attribute for form submission. | string | — | optional |
refThe rendered control, via `bind:ref`. | HTMLButtonElement | null | — | optional |
childrenCustom indicator content. Receives `{ checked }`. | Snippet<[{ checked: CheckboxChecked }]> | — | optional |
onCheckedChangeFires when the checked state changes (click or keyboard toggle). | (value: CheckboxChecked) => void | — | optional |
npm dependencies
Used by
Files installed (3)
src/lib/components/ui/checkbox/Checkbox.svelte10.5 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLButtonAttributes, MouseEventHandler } from 'svelte/elements' export type CheckboxChecked = boolean | 'indeterminate' export interface CheckboxProps extends Omit<HTMLButtonAttributes, 'value' | 'children'> { /** The controlled checked state. Bindable (`bind:checked`); falls back to internal state when unbound. */ checked?: CheckboxChecked | null /** The value given as data when submitted with a name. Inside a group, drives membership. */ value?: string /** Id of the element. Auto-generated when `label` is set without an explicit `id`. */ id?: string /** Size of the checkbox. */ size?: 'sm' | 'md' | 'lg' /** Custom color for the checked state — matches the Vuetify color system. */ color?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | string /** Label text displayed next to the checkbox. */ label?: string /** Hint text shown below the checkbox. */ hint?: string /** Error message(s) to display. */ errorMessages?: string | string[] /** Whether to show error state. */ error?: boolean /** When `true`, the checkbox is in readonly state (focusable but not toggleable). */ readonly?: boolean /** When `true`, shows an indeterminate state. */ indeterminate?: boolean /** Density of the checkbox — affects spacing. */ density?: 'compact' | 'default' | 'comfortable' /** When `true`, the icon is hidden. */ hideIcon?: boolean /** Loading state — shows a spinner. */ loading?: boolean /** Ripple effect on click (accepted for API parity; no visual effect). */ ripple?: boolean /** Custom icon to show when checked. */ checkedIcon?: Snippet /** Custom icon to show when indeterminate. */ indeterminateIcon?: Snippet /** Label position — before or after the checkbox. */ labelPosition?: 'before' | 'after' /** Whether the checkbox appears flat (no elevation). */ flat?: boolean /** Inline text style. */ inline?: boolean /** Name attribute for form submission. */ name?: string /** The rendered control, via `bind:ref`. */ ref?: HTMLButtonElement | null /** Custom indicator content. Receives `{ checked }`. */ children?: Snippet<[{ checked: CheckboxChecked }]> /** Fires when the checked state changes (click or keyboard toggle). */ onCheckedChange?: (value: CheckboxChecked) => void } </script> <script lang="ts"> import { getContext } from 'svelte' import { Check, Loader2, Minus } from '@lucide/svelte' import { cn } from '$lib/utils' import type { CheckboxGroupContext } from './CheckboxGroup.svelte' let { class: className, checked = $bindable(false), value, id, size = 'md', color = 'primary', label, hint, errorMessages, error = false, disabled = false, readonly = false, indeterminate = false, density = 'default', hideIcon = false, loading = false, ripple: _ripple = true, checkedIcon, indeterminateIcon, labelPosition = 'after', flat = false, inline = false, name, ref = $bindable(null), children, onCheckedChange, onclick: onclickProp, ...restProps }: CheckboxProps = $props() // `_ripple` is destructured aside (accepted for API parity with the Vue twin) // so it never leaks into `...restProps` and onto the DOM. const group = getContext<CheckboxGroupContext | null>('checkboxGroupContext') ?? null const inGroup = $derived(group !== null && value !== undefined) const actualName = $derived(name ?? group?.name) // Stable auto id for the `<label for>` association (no reactive useId in // Svelte — a module counter is enough for uniqueness within a page). let autoIdCounter = 0 const autoId = `checkbox-${++autoIdCounter}-${Math.random().toString(36).slice(2, 7)}` const resolvedId = $derived(id ?? (label ? autoId : undefined)) const sizeClasses = { sm: 'size-3.5', md: 'size-4', lg: 'size-5', } as const const iconSizes = { sm: 'size-2.5', md: 'size-3.5', lg: 'size-4', } as const const colorClasses: Record<string, string> = { primary: 'data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:border-primary', secondary: 'data-[state=checked]:bg-secondary data-[state=checked]:border-secondary data-[state=indeterminate]:bg-secondary data-[state=indeterminate]:border-secondary', success: 'data-[state=checked]:bg-success data-[state=checked]:border-success data-[state=indeterminate]:bg-success data-[state=indeterminate]:border-success', warning: 'data-[state=checked]:bg-warning data-[state=checked]:border-warning data-[state=indeterminate]:bg-warning data-[state=indeterminate]:border-warning', error: 'data-[state=checked]:bg-destructive data-[state=checked]:border-destructive data-[state=indeterminate]:bg-destructive data-[state=indeterminate]:border-destructive', info: 'data-[state=checked]:bg-info data-[state=checked]:border-info data-[state=indeterminate]:bg-info data-[state=indeterminate]:border-info', } const densityClasses = { compact: 'gap-1', default: 'gap-2', comfortable: 'gap-3', } as const const hasError = $derived( Boolean(error || (typeof errorMessages === 'string' ? errorMessages : (errorMessages?.length ?? 0) > 0)), ) const groupChecked = $derived(inGroup ? group!.isSelected(value!) : null) const isIndeterminate = $derived(indeterminate || checked === 'indeterminate') const dataState = $derived<'checked' | 'unchecked' | 'indeterminate'>( isIndeterminate ? 'indeterminate' : (groupChecked ?? checked ?? false) ? 'checked' : 'unchecked', ) const ariaChecked = $derived(dataState === 'indeterminate' ? 'mixed' : dataState === 'checked') const checkboxClasses = $derived( cn( 'peer border-input data-[state=checked]:text-primary-foreground data-[state=indeterminate]:text-primary-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shrink-0 rounded-[4px] border shadow-xs transition-colors duration-200 outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50', sizeClasses[size], colorClasses[color] || colorClasses.primary, hasError && 'border-destructive data-[state=checked]:!bg-destructive data-[state=checked]:!border-destructive data-[state=indeterminate]:!bg-destructive data-[state=indeterminate]:!border-destructive', flat && 'shadow-none', className, ), ) const handleClick: MouseEventHandler<HTMLButtonElement> = (event) => { toggle() onclickProp?.(event) } function toggle() { if (disabled || group?.disabled || readonly || loading) return if (inGroup) { group!.toggle(value!) onCheckedChange?.(group!.isSelected(value!) ? true : false) return } const next: CheckboxChecked = dataState === 'checked' ? false : true checked = next onCheckedChange?.(next) } </script> <div class={cn('flex items-start', densityClasses[density], inline ? 'inline-flex' : 'flex-col')}> {#if label && labelPosition === 'before'} <label for={resolvedId} class={cn( 'mr-2 cursor-pointer text-sm leading-none font-medium select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', hasError ? 'text-destructive' : '', disabled && 'cursor-not-allowed opacity-50', )} > {label} </label> {/if} <div class="flex items-center"> <button bind:this={ref} type="button" role="checkbox" data-uipkge data-slot="checkbox" id={resolvedId} aria-checked={ariaChecked} aria-invalid={hasError || undefined} data-state={dataState} {value} disabled={disabled || group?.disabled} class={checkboxClasses} onclick={handleClick} {...restProps} > <span data-slot="checkbox-indicator" class="grid place-content-center text-current transition-none"> {#if children} {@render children({ checked: dataState === 'checked' ? true : dataState === 'indeterminate' ? 'indeterminate' : false, })} {:else if loading} <Loader2 class={cn(iconSizes[size], 'animate-spin')} /> {:else if isIndeterminate} {#if indeterminateIcon} {@render indeterminateIcon()} {:else} <Minus class={cn(iconSizes[size], 'checkbox-indicator-icon')} /> {/if} {:else if !hideIcon && dataState === 'checked'} {#if checkedIcon} {@render checkedIcon()} {:else} <Check class={cn(iconSizes[size], 'checkbox-indicator-icon')} /> {/if} {/if} </span> </button> {#if actualName} <input type="checkbox" hidden tabindex="-1" aria-hidden="true" name={actualName} value={value ?? 'on'} checked={dataState === 'checked'} /> {/if} {#if label && labelPosition === 'after'} <label for={resolvedId} class={cn( 'ml-2 cursor-pointer text-sm leading-none font-medium select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', hasError ? 'text-destructive' : '', disabled && 'cursor-not-allowed opacity-50', )} > {label} </label> {/if} </div> {#if hint && !hasError} <p class="text-muted-foreground mt-1 text-xs"> {hint} </p> {/if} {#if hasError} <div class="mt-1 flex flex-col gap-0.5"> {#if typeof errorMessages === 'string'} <p class="text-destructive text-xs">{errorMessages}</p> {:else} {#each errorMessages ?? [] as msg (msg)} <p class="text-destructive text-xs">{msg}</p> {/each} {/if} </div> {/if} </div> <style> /* Subtle scale-in when the check / indeterminate mark mounts. */ @keyframes checkbox-check-in { 0% { opacity: 0; transform: scale(0.55); } 70% { opacity: 1; transform: scale(1.08); } 100% { opacity: 1; transform: scale(1); } } :global([data-slot='checkbox-indicator']) .checkbox-indicator-icon { animation: checkbox-check-in 200ms cubic-bezier(0.22, 1.2, 0.36, 1) both; } @media (prefers-reduced-motion: reduce) { :global([data-slot='checkbox-indicator']) .checkbox-indicator-icon { animation: none !important; } } </style>src/lib/components/ui/checkbox/CheckboxGroup.svelte4.5 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLAttributes } from 'svelte/elements' export interface CheckboxOption { label: string value: string disabled?: boolean } export interface CheckboxGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> { /** Checked values. Bindable (`bind:value`). */ value?: string[] /** Initial checked values when `value` is unbound. */ defaultValue?: string[] /** When `true`, prevents interaction with all checkboxes. */ disabled?: boolean /** Whether the group is in an error state. */ error?: boolean /** Error messages to display. */ errorMessages?: string | string[] /** Label for the group. */ label?: string /** Hint text for the group. */ hint?: string /** The orientation of the checkboxes. */ orientation?: 'horizontal' | 'vertical' /** Whether to show a border around the group. */ bordered?: boolean /** Density of the checkboxes. */ density?: 'compact' | 'default' | 'comfortable' /** Options to render as checkboxes automatically. */ options?: (string | CheckboxOption)[] /** Name attribute for all checkboxes in the group. */ name?: string /** Inline layout (alias for horizontal). */ inline?: boolean /** Fires when the checked values change. */ onValueChange?: (value: string[]) => void children?: Snippet<[{ value: string[] }]> } /** Hand-rolled group context (replaces reka-ui's CheckboxGroupRoot state). */ export interface CheckboxGroupContext { readonly name: string | undefined readonly disabled: boolean isSelected: (value: string) => boolean toggle: (value: string) => void } </script> <script lang="ts"> import { setContext } from 'svelte' import { cn } from '$lib/utils' import Checkbox from './Checkbox.svelte' let { class: className, value = $bindable(), defaultValue = [], disabled = false, error = false, errorMessages, label, hint, orientation = 'vertical', bordered = false, density = 'default', options, name, inline = false, onValueChange, children, ...restProps }: CheckboxGroupProps = $props() // Intentional one-shot seed: later `defaultValue` changes must not clobber user selection. // svelte-ignore state_referenced_locally let internalValue = $state<string[]>([...defaultValue]) const resolved = $derived(value ?? internalValue) const actualOrientation = $derived(inline ? 'horizontal' : orientation) function isSelected(v: string) { return resolved.includes(v) } function toggle(v: string) { if (disabled) return const next = isSelected(v) ? resolved.filter((item) => item !== v) : [...resolved, v] if (value === undefined) internalValue = next value = next onValueChange?.(next) } setContext<CheckboxGroupContext>('checkboxGroupContext', { get name() { return name }, get disabled() { return disabled }, isSelected, toggle, }) </script> <div role="group" data-uipkge data-slot="checkbox-group" data-orientation={actualOrientation} class={cn( 'flex flex-col gap-2', actualOrientation === 'horizontal' ? 'flex-row items-center' : 'flex-col', bordered && 'rounded-lg border p-4', className, )} {...restProps} > {#if label} <span class="text-sm font-medium">{label}</span> {/if} {#if hint && !error} <p class="text-muted-foreground text-xs">{hint}</p> {/if} <div class={cn('flex gap-4', actualOrientation === 'horizontal' ? 'flex-row flex-wrap items-center' : 'flex-col')}> {#if options && options.length > 0} {#each options as option, i (typeof option === 'string' ? option : `${option.value}-${i}`)} <Checkbox value={typeof option === 'string' ? option : option.value} label={typeof option === 'string' ? option : option.label} disabled={disabled || (typeof option === 'string' ? undefined : option.disabled)} {name} {density} /> {/each} {:else if children} {@render children({ value: resolved })} {/if} </div> {#if error || errorMessages} <div class="flex flex-col gap-0.5"> {#if typeof errorMessages === 'string'} <p class="text-destructive text-xs">{errorMessages}</p> {:else} {#each errorMessages ?? [] as msg (msg)} <p class="text-destructive text-xs">{msg}</p> {/each} {/if} </div> {/if} </div>src/lib/components/ui/checkbox/index.ts0.2 kB
export { default as Checkbox, type CheckboxChecked, type CheckboxProps } from './Checkbox.svelte' export { default as CheckboxGroup, type CheckboxGroupContext, type CheckboxGroupProps, type CheckboxOption, } from './CheckboxGroup.svelte'
Raw manifest:https://uipkge.dev/r/svelte/checkbox.json