---
title: Checkbox
description: Capture an independent checked, unchecked, or mixed choice.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# Checkbox

> Capture an independent checked, unchecked, or mixed choice.

Use Checkbox for an independent boolean or mapped choice that can be selected alongside other choices. Use [Switch](https://moraine.subf.dev/components/switch.md) when the control represents an immediate on/off setting.

## Basic usage

```tsx
import { Checkbox } from 'moraine'

export function Example() {
  return <Checkbox label="Accept terms" />
}
```

## Anatomy

```text
Checkbox [component; slot=root]
├── container [slot]
│   ├── input [internal]
│   └── control [slot]
│       └── indicator [slot]
│           └── icon [slot]
└── wrapper [slot]
    ├── label [slot]
    └── description [slot]
```

## Usage

### State and form values

Use `checked` with `onCheckedChange` for controlled state, or `defaultChecked` for an initial value.
Set `indeterminate` from aggregate selection state; it changes the mixed presentation without
adding a third submitted value.

`trueValue` and `falseValue` map checked state to domain values for callbacks and schema-bound
forms. Native form submission uses the separate `value` prop only while checked; an unchecked
checkbox contributes no entry.

```tsx
import { Checkbox } from 'moraine'
import { createSignal } from 'solid-js'

export function StateAndValues() {
  const [checked, setChecked] = createSignal(true)

  return (
    <div class="flex flex-col gap-3">
      <Checkbox
        checked={checked()}
        onCheckedChange={setChecked}
        label="Subscribe to product updates"
        description="Receive weekly summaries of new releases and features."
      />
      <p class="text-xs text-muted-foreground">
        Current state:{' '}
        <span class="text-foreground font-medium">{checked() ? 'checked' : 'unchecked'}</span>
      </p>
    </div>
  )
}
```

### Read-only and disabled

`disabled` removes the control from normal interaction. `readOnly` preserves the value and focusability while preventing changes.

```tsx
import { Checkbox } from 'moraine'

export function DisabledReadonly() {
  return (
    <div class="flex flex-col gap-4">
      <Checkbox
        disabled
        defaultChecked
        label="Disabled (checked)"
        description="Non-interactive and removed from form submission."
      />
      <Checkbox
        readOnly
        defaultChecked
        label="Read-only (checked)"
        description="Focusable and submitted, but prevents value changes."
      />
    </div>
  )
}
```

## Examples

### Indeterminate and custom indicator

```tsx
import { Checkbox } from 'moraine'
import { createMemo, createSignal, For } from 'solid-js'

export function IndeterminateCustomIcons() {
  const [tasks, setTasks] = createSignal([
    { id: 'tests', label: 'Run unit and integration test suites', checked: true },
    { id: 'build', label: 'Build static assets and server bundle', checked: true },
    { id: 'migrate', label: 'Apply pending database migrations', checked: false },
  ])

  const checkedCount = createMemo(() => tasks().filter((t) => t.checked).length)
  const allChecked = createMemo(() => checkedCount() === tasks().length)
  const isIndeterminate = createMemo(() => checkedCount() > 0 && checkedCount() < tasks().length)

  const parentState = createMemo<'indeterminate' | boolean>(() => {
    if (isIndeterminate()) {
      return 'indeterminate'
    }
    return allChecked()
  })

  const toggleAll = () => {
    const nextState = !allChecked()
    setTasks((current) => current.map((task) => ({ ...task, checked: nextState })))
  }

  const toggleTask = (id: string) => {
    setTasks((current) =>
      current.map((task) => (task.id === id ? { ...task, checked: !task.checked } : task)),
    )
  }

  return (
    <div class="p-4 b-1 b-border rounded-xl max-w-xl space-y-4">
      <Checkbox
        label="Production deployment checklist"
        description={`${checkedCount()} of ${tasks().length} tasks completed`}
        checked={parentState()}
        onCheckedChange={toggleAll}
        checkedIcon="i-lucide:check-check"
        indeterminateIcon="i-lucide:minus"
      />

      <div class="pl-6 border-border border-l-2 space-y-2">
        <For each={tasks()}>
          {(task) => (
            <Checkbox
              size="sm"
              label={task.label}
              checked={task.checked}
              onCheckedChange={() => toggleTask(task.id)}
            />
          )}
        </For>
      </div>
    </div>
  )
}
```

### Custom true and false values

```tsx
import { Badge, Checkbox } from 'moraine'
import { createSignal } from 'solid-js'

export function CustomTrueFalseValues() {
  const [telemetry, setTelemetry] = createSignal<'opted-in' | 'opted-out'>('opted-in')

  return (
    <div class="p-4 b-1 b-border rounded-xl max-w-xl space-y-4">
      <div class="flex gap-4 items-center justify-between">
        <Checkbox<'opted-in', 'opted-out'>
          label="Anonymous telemetry collection"
          description="Send anonymous crash diagnostics and performance reports."
          trueValue="opted-in"
          falseValue="opted-out"
          checked={telemetry()}
          onCheckedChange={setTelemetry}
        />
        <Badge variant={telemetry() === 'opted-in' ? 'subtle' : 'outline'}>{telemetry()}</Badge>
      </div>

      <p class="text-xs text-muted-foreground">
        Controlled domain state is stored as <code class="font-mono">{telemetry()}</code> instead of
        a raw boolean.
      </p>
    </div>
  )
}
```

### Form integration

```tsx
import { Button, Checkbox, createForm } from 'moraine'
import { createSignal } from 'solid-js'
import * as v from 'valibot'

export function FormIntegration() {
  const [submitted, setSubmitted] = createSignal<boolean | null>(null)
  const form = createForm({
    schema: v.object({
      terms: v.pipe(v.boolean(), v.literal(true, 'You must accept the terms to proceed.')),
    }),
    initialInput: { terms: false },
    validate: 'input',
  })

  return (
    <form.Form onSubmit={(output) => setSubmitted(output.terms)}>
      <div class="max-w-xl space-y-4">
        <form.Field name="terms" description="Required for account activation." required>
          <Checkbox label="I agree to the Terms of Service and Privacy Policy" />
        </form.Field>
        <div class="flex gap-3 items-center">
          <Button type="submit" variant="secondary" size="sm">
            Validate
          </Button>
          <p class="text-xs text-muted-foreground">
            Terms accepted: {submitted() === null ? 'Pending' : submitted() ? 'Yes' : 'No'}
          </p>
        </div>
      </div>
    </form.Form>
  )
}
```

## Attributes

| Attributes | Slot | Description |
| --- | --- | --- |
| `data-checked` | `checkbox`, `checkbox-control`, `checkbox-indicator` | Present when the item or toggle is checked. |
| `data-disabled` | `checkbox`, `checkbox-control`, `checkbox-indicator` | Present when the component, slot, or item is disabled. |
| `data-indeterminate` | `checkbox`, `checkbox-control`, `checkbox-indicator` | Present when the checkbox or progress state is indeterminate. |
| `data-invalid` | `checkbox`, `checkbox-control` | Present when the field or form has a validation error. |
| `data-readonly` | `checkbox`, `checkbox-control` | Present when the field is in read-only mode. |
| `data-required` | `checkbox`, `checkbox-control`, `checkbox-label` | Present when the field input is required. |
| `data-unchecked` | `checkbox`, `checkbox-control` | Present when the checkbox, radio item, or switch is unchecked. |

## Props

Props for the Checkbox component.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| checked | TTrue \| TFalse \| 'indeterminate' \| undefined | — | Whether the checkbox is checked (controlled). |
| checkedIcon | IconT.Name \| undefined | 'icon-check' | Icon to show when checked. |
| defaultChecked | boolean \| 'indeterminate' \| undefined | false | Whether the checkbox is checked by default (uncontrolled). |
| description | JSX.Element \| undefined | — | Description text for the checkbox. |
| disabled | boolean \| undefined | false | Whether the input is disabled. |
| falseValue | TFalse \| undefined | false | Value to use when the checkbox is unchecked. |
| fieldBind | boolean \| undefined | true | Whether to bind the checkbox value to the parent Field. |
| id | string \| undefined | — | The ID of the input element. |
| indeterminate | boolean \| undefined | false | Whether the checkbox is in an indeterminate state. |
| indeterminateIcon | IconT.Name \| undefined | 'icon-minus' | Icon to show when indeterminate. |
| indicator | 'start' \| 'end' \| 'hidden' \| undefined | 'start' | Placement of the selection indicator. |
| inputRef | Ref<HTMLInputElement> \| undefined | — | Native input element ref. |
| label | JSX.Element \| undefined | — | Label for the checkbox. |
| name | string \| undefined | — | The name of the input element, used for form submission. |
| onCheckedChange | ((value: TTrue \| TFalse) => void) \| undefined | — | Callback when the checked state changes. |
| onPointerDown | JSX.EventHandlerUnion<HTMLButtonElement, PointerEvent> \| undefined | — | Pointer down handler for the checkbox control. |
| readOnly | boolean \| undefined | false | Whether the input is read-only. |
| required | boolean \| undefined | false | Whether the input is required. |
| size | 'sm' \| 'md' \| 'lg' \| undefined | 'md' | Visual size of the component. |
| trueValue | TTrue \| undefined | true | Value to use when the checkbox is checked. |
| value | string \| undefined | 'on' | Native value submitted when the checkbox is checked. |
| variant | 'card' \| 'list' \| undefined | — | Visual treatment of the component. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | Classes \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | Styles \| undefined | — | Family slot style defaults for this instance. |
