# Checkbox

A checkbox with a third, indeterminate state, for a choice a form submits.

Checkbox is a tri-state checkbox that takes part in forms. Useful wherever a
choice should only count once the form is submitted; for a setting that applies
immediately, use [`Switch`](/components/switch).

::component-preview{name="CheckboxPreview"}
```vue
<template>
  <div class="flex flex-col gap-3">
    <Field.Root
      v-for="option in options"
      :key="option.name"
      class="flex items-start gap-3"
      :options="{ disabled: option.disabled, name: option.name }"
    >
      <Checkbox.Root
        v-model="option.checked"
        v-model:indeterminate="option.indeterminate"
        :class="box"
      >
        <Checkbox.Indicator class="block size-[0.9375rem]">
          <svg viewBox="0 0 18 18" fill="currentColor" aria-hidden="true">
            <path v-if="option.indeterminate" :d="indeterminate" />
            <path v-else :d="check" />
          </svg>
        </Checkbox.Indicator>
      </Checkbox.Root>

      <Label :class="label">{{ option.label }}</Label>
    </Field.Root>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Checkbox, Field, Label } from '@maas/mirror/vue'

const box = [
  'flex size-5 items-center justify-center border-2 border-surface',
  'rounded-component-compact-md text-accent-on-solid',
  'transition-all duration-100 ease-linear',
  'outline-4 outline-transparent focus-visible:focus-ring',
  'active:border-primary-subtle',
  'data-[state=checked]:bg-accent-solid data-[state=indeterminate]:bg-accent-solid',
  'data-[state=checked]:border-[transparent] data-[state=indeterminate]:border-[transparent]',
  'data-[state=checked]:active:bg-accent-solid-active',
  'data-[state=indeterminate]:active:bg-accent-solid-active',
  'data-[disabled=true]:border-disabled-subtle',
  'data-[disabled=true]:data-[state=checked]:bg-disabled-solid',
  'data-[disabled=true]:text-disabled-on-solid',
].join(' ')

const label = [
  'type-component-xs text-surface translate-y-[0.1rem] [--mirror-label-cursor:pointer]',
  'data-[disabled=true]:text-disabled-solid',
].join(' ')

const check =
  'M13.6875 5.1875C13.875 5 14.1042 4.90625 14.375 4.90625C14.6458 4.90625 14.875 5 15.0625 5.1875C15.25 5.375 15.3438 5.60417 15.3438 5.875C15.3438 6.14583 15.25 6.375 15.0625 6.5625L7.96875 13.6562C7.63542 13.9896 7.22917 14.1562 6.75 14.1562C6.27083 14.1562 5.86458 13.9896 5.53125 13.6562L2.9375 11.0625C2.75 10.875 2.65625 10.6458 2.65625 10.375C2.65625 10.1042 2.75 9.875 2.9375 9.6875C3.125 9.5 3.35417 9.40625 3.625 9.40625C3.89583 9.40625 4.125 9.5 4.3125 9.6875L6.90625 12.2812C6.86458 12.2396 6.8125 12.2188 6.75 12.2188C6.6875 12.2188 6.63542 12.2396 6.59375 12.2812L13.6875 5.1875Z'

const indeterminate =
  'M14.0313 8.03125C14.3021 8.03125 14.5312 8.125 14.7187 8.3125C14.9062 8.5 15 8.72917 15 9C15 9.27083 14.9062 9.5 14.7187 9.6875C14.5312 9.875 14.3021 9.96875 14.0313 9.96875H3.96875C3.69792 9.96875 3.46875 9.875 3.28125 9.6875C3.09375 9.5 3 9.27083 3 9C3 8.72917 3.09375 8.5 3.28125 8.3125C3.46875 8.125 3.69792 8.03125 3.96875 8.03125H14.0313Z'

const options = ref([
  {
    name: 'terms',
    label: 'Accept the terms',
    checked: true,
    indeterminate: false,
    disabled: false,
  },
  {
    name: 'digest',
    label: 'Weekly digest',
    checked: false,
    indeterminate: true,
    disabled: false,
  },
  {
    name: 'beta',
    label: 'Beta features',
    checked: false,
    indeterminate: false,
    disabled: true,
  },
])
</script>
```
::

## Usage guidelines

- Make sure the checkbox has an accessible name, either through a
  [`Label`](/components/label) inside a [`Field`](/components/field) or an
  `aria-label` on the root.
- Bind the mixed state with `v-model:indeterminate`. Passed as a plain prop it
  is controlled, so activating the checkbox emits but leaves the mixed state on
  screen.
- `options.readOnly` never reaches the hidden input, so a read-only checkbox
  still submits its value. Use `options.disabled` to leave it out of the
  submission.

## Anatomy

Assemble the root and its indicator.

::component-anatomy
---
parts:
  - name: Checkbox.Root
    required: true
    description: Renders a button with role="checkbox", plus a visually hidden checkbox input.
    children:
      - name: Checkbox.Indicator
        description: Renders a span. Unmounted while unchecked.
---
::

```vue
<script setup lang="ts">
import { Checkbox, Field, Label } from '@maas/mirror/vue'
</script>

<template>
  <Field.Root :options="{ name: 'terms' }">
    <Checkbox.Root v-model="accepted" v-model:indeterminate="partial">
      <Checkbox.Indicator>
        <IconDash v-if="partial" />
        <IconCheck v-else />
      </Checkbox.Indicator>
    </Checkbox.Root>
    <Label>I accept the terms</Label>
  </Field.Root>
</template>
```

## Examples

### Controlled and uncontrolled

Leave `modelValue` out and the checkbox keeps its own state, seeded by
`defaultValue`. Bind `v-model` and you own it instead, which is
[decided once at mount](/components/composition#controlled-and-uncontrolled).

```vue
<template>
  <Checkbox.Root default-value>
    <Checkbox.Indicator>✓</Checkbox.Indicator>
  </Checkbox.Root>

  <Checkbox.Root v-model="accepted">
    <Checkbox.Indicator>✓</Checkbox.Indicator>
  </Checkbox.Root>
</template>

<script setup lang="ts">
const accepted = ref(false)
</script>
```

### Indeterminate

`indeterminate` is independent of the checked state, so the two are reported
separately in `data-state`, `data-checked` and `data-indeterminate`.

```vue
<template>
  <Checkbox.Root v-model="checked" v-model:indeterminate="partial">
    <Checkbox.Indicator>
      {{ partial ? '–' : '✓' }}
    </Checkbox.Indicator>
  </Checkbox.Root>
</template>

<script setup lang="ts">
const checked = ref(false)
const partial = ref(true)
</script>
```

Activating an indeterminate checkbox sets it checked and clears indeterminate,
the way a native tri-state input behaves.

### Driving a list of children

Work both flags out from the children, and write back when either changes.

```vue
<template>
  <Checkbox.Root
    :indeterminate="some && !all"
    :model-value="all"
    @update:model-value="setAll"
  >
    <Checkbox.Indicator>{{ all ? '✓' : '–' }}</Checkbox.Indicator>
  </Checkbox.Root>
</template>

<script setup lang="ts">
const items = ref([
  { id: 'a', checked: false },
  { id: 'b', checked: true },
])

const all = computed(() => items.value.every((item) => item.checked))
const some = computed(() => items.value.some((item) => item.checked))

function setAll(next: boolean) {
  items.value.forEach((item) => (item.checked = next))
}
</script>
```

### Inside a form

The hidden input takes `name`, `value`, `required` and `disabled`, so
[the form sees it](/components/form-integration#form-participation), and a surrounding `Field` supplies each of them.

```vue
<template>
  <form>
    <Field.Root :options="{ name: 'terms', required: true }">
      <Checkbox.Root>
        <Checkbox.Indicator>✓</Checkbox.Indicator>
      </Checkbox.Root>
      <Label>I accept the terms</Label>
    </Field.Root>
  </form>
</template>
```

An unchecked checkbox submits nothing, which is the native behaviour. If you
need a value either way, set `uncheckedValue` and a second hidden input holds it
while the checkbox is unchecked.

```vue
<template>
  <Checkbox.Root
    :options="{ name: 'terms', uncheckedValue: 'no', value: 'yes' }"
  >
    <Checkbox.Indicator>✓</Checkbox.Indicator>
  </Checkbox.Root>
</template>
```

A checkbox rendered outside the `<form>` it belongs to, in a dialog or a
sticky footer, points at it by `id` through `form`. Both hidden inputs take the
attribute.

```vue
<template>
  <form id="signup">…</form>

  <Checkbox.Root :options="{ form: 'signup', name: 'terms' }">
    <Checkbox.Indicator>✓</Checkbox.Indicator>
  </Checkbox.Root>
</template>
```

### Reaching the hidden input

The element the form actually submits is exposed as `input` on the component
instance, for the rare case that wants to read `validity` or call
`setCustomValidity()` on it.

```vue
<template>
  <Checkbox.Root ref="terms" :options="{ name: 'terms', required: true }" />
</template>

<script setup lang="ts">
const terms = useTemplateRef('terms')

function report() {
  terms.value?.input?.reportValidity()
}
</script>
```

### Styling from state

Paint the root from `data-state`, and read the same values as slot props where
the markup itself has to branch.

```vue
<template>
  <Checkbox.Root class="checkbox">
    <Checkbox.Indicator v-slot="{ indeterminate }">
      {{ indeterminate ? '–' : '✓' }}
    </Checkbox.Indicator>
  </Checkbox.Root>
</template>

<style>
.checkbox[data-state='checked'],
.checkbox[data-state='indeterminate'] {
  background: var(--app-color-primary-bg-solid);
}

.checkbox[data-disabled='true'] {
  background: var(--app-color-disabled-bg-subtle);
}
</style>
```

The indicator is unmounted while the checkbox is unchecked. If you would rather
animate it yourself, `options.forceMount` keeps it in the DOM in every state.

```vue
<template>
  <Checkbox.Root :options="{ forceMount: true }">
    <Checkbox.Indicator>✓</Checkbox.Indicator>
  </Checkbox.Root>
</template>
```

## API reference

**Module.** A bundled `options` object, and `useMirrorCheckbox(id)`
as the programmatic API.

### `Checkbox.Root`

Renders a `<button role="checkbox">` plus a visually hidden
`<input type="checkbox">`, as a sibling of the button rather than a child.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: 'The instance ID, for [`useMirrorCheckbox`](#composable).'
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: modelValue
        description: The checked state. `v-model`.
      - label: 'boolean | undefined'
      - label: undefined
  - items:
      - label: defaultValue
        description: Initial state when [uncontrolled](#controlled-and-uncontrolled).
      - label: boolean
      - label: 'false'
  - items:
      - label: indeterminate
        description: 'The [mixed state](#indeterminate). `v-model:indeterminate`.'
      - label: 'boolean | undefined'
      - label: undefined
  - items:
      - label: options
        description: Everything else. Deep-merged over the defaults.
      - label: CheckboxOptions
      - label: see below
        plaintext: true
---
::

#### Options

::docs-table
---
columns:
  - label: Option
  - label: Type
  - label: Default
rows:
  - items:
      - label: name
        description: 'Form field name, on [the hidden input](#inside-a-form).'
      - label: string
      - label: inherited from `Field`
        plaintext: true
  - items:
      - label: form
        description: The `id` of the form, when rendered outside it.
      - label: string
      - label: undefined
  - items:
      - label: value
        description: Submitted value while checked.
      - label: string
      - label: '''on'''
  - items:
      - label: uncheckedValue
        description: 'Submitted while [unchecked](#inside-a-form), through a second hidden input.'
      - label: string
      - label: undefined
  - items:
      - label: parent
        description: 'Makes this the parent checkbox of a surrounding [`Checkbox.Group`](/components/checkbox-group#the-parent-checkbox).'
      - label: boolean
      - label: 'false'
  - items:
      - label: forceMount
        description: 'Keeps [the indicator](#styling-from-state) mounted in every state.'
      - label: boolean
      - label: 'false'
  - items:
      - label: transition
        description: 'Vue transition name for the indicator. [Timing lives in CSS](/components/animation).'
      - label: string
      - label: '''mirror-checkbox-indicator'''
  - items:
      - label: disabled
        description: Blocks changes.
      - label: boolean
      - label: inherited from `Field`, else `false`
        plaintext: true
  - items:
      - label: readOnly
        description: Blocks changes but keeps the control focusable.
      - label: boolean
      - label: inherited from `Field`, else `false`
        plaintext: true
  - items:
      - label: required
        description: Marks the hidden input required.
      - label: boolean
      - label: inherited from `Field`, else `false`
        plaintext: true
---
::

A disabled or read-only `Checkbox.Group` settles both flags for every checkbox
inside it, whatever their own options say.

Inside a [`Checkbox.Group`](/components/checkbox-group) the group owns
submission, so `name`, `form`, `required` and `uncheckedValue` stop meaning
anything on the member and warn `ignored_member_options` in development. Set
them on the group instead.

#### Emits

::docs-table
---
columns:
  - label: Emit
  - label: Payload
rows:
  - items:
      - label: update:modelValue
        description: The checked state changes.
      - label: boolean
  - items:
      - label: update:indeterminate
        description: An indeterminate checkbox is activated, resolving it to `false`.
      - label: boolean
---
::

#### Slot props

The [field state set](/components/styling#the-field-state-set), plus the two below.

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: checked
        description: The checked state, independent of `indeterminate`.
      - label: boolean
  - items:
      - label: indeterminate
        description: Mirrors `data-indeterminate`.
      - label: boolean
---
::

`Checkbox.Root` also exposes `input`, the hidden `<input type="checkbox">`
element, on its instance.

### `Checkbox.Indicator`

Renders a `<span>`, mounted while the checkbox is checked or indeterminate.

A root has one indicator, so `forceMount` and `transition` are answered by the
root’s options and the part takes no props beyond `id`.

#### Slot props

The same as the root.

### Composable

`useMirrorCheckbox(id)` reaches a checkbox from anywhere in the app.

::docs-table
---
columns:
  - label: Key
  - label: Type
rows:
  - items:
      - label: checked
        description: The checked state.
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: indeterminate
        description: The mixed state.
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: disabled
        description: Mirrors `data-disabled`.
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: readOnly
        description: Mirrors `data-readonly`.
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: setChecked
        description: Sets the checked state.
      - label: '(next: boolean) => void'
        escape: true
  - items:
      - label: setIndeterminate
        description: Sets the mixed state.
      - label: '(next: boolean) => void'
        escape: true
  - items:
      - label: toggle
        description: Flips the checked state.
      - label: () => void
        escape: true
---
::

### Data attributes

Both parts write the [field state set](/components/styling#the-field-state-set), plus the ones below.

::data-attributes
::

### CSS variables

::css-variables
::

### Errors

::docs-table
---
columns:
  - label: Code
rows:
  - items:
      - label: missing_checkbox_context
        description: '`Checkbox.Indicator` rendered outside `Checkbox.Root`.'
---
::

## Accessibility

The rendered element takes `role="checkbox"` with `aria-checked` as `true`,
`false` or `mixed`, `aria-required` follows `required`, and a non-native
checkbox reports `aria-disabled` rather than leaving the tab order.

::docs-table
---
columns:
  - label: Key
  - label: Action
rows:
  - items:
      - label: Space
      - label: Toggles, on `keyup`.
        plaintext: true
  - items:
      - label: Enter
      - label: Nothing. The `keydown` default is prevented, so Enter neither toggles the checkbox nor submits the surrounding form.
        plaintext: true
---
::
