---
language: "en"
title: "Checkbox"
canonical: "https://uniaria.tenphi.me/components/checkbox/"
source: "docs/components/checkbox.mdx"
index: true
---

# Checkbox

A checkbox turns one option on or off. A native `<input type="checkbox">` sits
underneath, and the browser keeps doing the toggling: a click on the input or
its label, or Space, toggles it, and the form submits it. UniAria labels it
and links its description and error message (as [Field](/components/field/) does),
and adds the indeterminate and read-only states, controlled use, and state
attributes on every part for your CSS.

It follows the WAI-ARIA [Checkbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/),
with the native input's role and state in place of `role="checkbox"` and
`aria-checked`.

## Usage

```tsx
import { checkbox } from '@uniaria/core/checkbox';
import { useBehavior } from '@uniaria/react';

export function TermsCheckbox(props: {
  isInvalid: boolean;
  onAcceptedChange(isAccepted: boolean): void;
}) {
  const terms = useBehavior(checkbox, {
    labelElementType: 'span',
    isRequired: true,
    isInvalid: props.isInvalid,
    onSelectedChange: props.onAcceptedChange,
  });
  return (
    <div {...terms.parts.root}>
      <label {...terms.parts.wrapper}>
        <input {...terms.parts.input} name="terms" />
        <span {...terms.parts.indicator} />
        <span {...terms.parts.label}>Accept the terms</span>
      </label>
      <p {...terms.parts.error}>You must accept the terms to continue.</p>
    </div>
  );
}
```

```vue
<script setup lang="ts">
import { checkbox } from '@uniaria/core/checkbox';
import { useBehavior } from '@uniaria/vue';

const props = defineProps<{ isInvalid: boolean }>();
const emit = defineEmits<{ acceptedChange: [isAccepted: boolean] }>();

const terms = useBehavior(checkbox, () => ({
  labelElementType: 'span',
  isRequired: true,
  isInvalid: props.isInvalid,
  onSelectedChange: (isSelected) => emit('acceptedChange', isSelected),
}));
</script>

<template>
  <div v-bind="terms.parts.root">
    <label v-bind="terms.parts.wrapper">
      <input v-bind="terms.parts.input" name="terms" />
      <span v-bind="terms.parts.indicator" />
      <span v-bind="terms.parts.label">Accept the terms</span>
    </label>
    <p v-bind="terms.parts.error">You must accept the terms to continue.</p>
  </div>
</template>
```

```html
<uni-checkbox id="terms" required>
  <div data-uni="checkbox">
    <label data-element="Wrapper">
      <input data-element="Input" type="checkbox" name="terms" />
      <span data-element="Indicator"></span>
      <span data-element="Label">Accept the terms</span>
    </label>
    <p data-element="Error" hidden>You must accept the terms to continue.</p>
  </div>
</uni-checkbox>

<script type="module">
  import '@uniaria/elements/checkbox';

  const terms = document.querySelector('#terms');
  terms.addEventListener('selectedchange', (event) => {
    terms.isInvalid = !event.detail;
  });
</script>
```

The input's `checked` in the markup, or a click before the script loaded,
becomes the element's starting state.

The error part shows only while the checkbox is invalid and not disabled;
UniAria toggles its `hidden` attribute and announces it when it appears. See
[Field](/components/field/) for labelling, descriptions and errors.

### Two layouts

A click toggles the checkbox natively in both, with no script:

* **A `label` around the input**, the indicator and a `span` label part, so
  a click anywhere in it toggles. Labels can't nest, so in React and Vue pass
  `labelElementType: 'span'`; the custom element reads the tag itself. The
  `label` is the `wrapper` part, and the root goes around it, the
  description and the error, which stay outside the `label` so a click on
  them doesn't toggle. The usage above has this layout. A checkbox with
  neither can make the `label` its root, with no wrapper, as the demo does.
* **A `div` root** with a `label` label part, whose `for` UniAria points at
  the input. Only the input and the label toggle; an indicator outside the
  label doesn't.

```tsx
const newsletter = useBehavior(checkbox, { defaultSelected: true });
return (
  <div {...newsletter.parts.root}>
    <input {...newsletter.parts.input} name="newsletter" />
    <label {...newsletter.parts.label}>Subscribe to the newsletter</label>
    <p {...newsletter.parts.description}>One email a month.</p>
  </div>
);
```

```vue
<script setup lang="ts">
const newsletter = useBehavior(checkbox, { defaultSelected: true });
</script>

<template>
  <div v-bind="newsletter.parts.root">
    <input v-bind="newsletter.parts.input" name="newsletter" />
    <label v-bind="newsletter.parts.label">Subscribe to the newsletter</label>
    <p v-bind="newsletter.parts.description">One email a month.</p>
  </div>
</template>
```

```html
<uni-checkbox>
  <div data-uni="checkbox">
    <input data-element="Input" type="checkbox" name="newsletter" checked />
    <label data-element="Label">Subscribe to the newsletter</label>
    <p data-element="Description">One email a month.</p>
  </div>
</uni-checkbox>
```

To draw your own box, hide the native input from sight but not from the
page: lay it over the `label` with `opacity: 0`, as the demo does, or clip
it. Never `display: none`, `visibility: hidden` or `hidden`, which take it out
of the tab order and the accessibility tree.

### Indeterminate

`isIndeterminate` shows a dash: it sets the input's `indeterminate` property
and `data-indeterminate` on every part. It is presentational and independent
of `isSelected`. A click selects an indeterminate checkbox that isn't
selected, as the browser does, and the dash stays as long as the prop is
true: you clear it. A "select all" checkbox computes both from its rows.

```tsx
const all = useBehavior(checkbox, {
  labelElementType: 'span',
  isSelected: selected.length === toppings.length,
  isIndeterminate: selected.length > 0 && selected.length < toppings.length,
  onSelectedChange: (isSelected) => setSelected(isSelected ? toppings : []),
});
```

```vue
<script setup lang="ts">
const toppings = ['cheese', 'olives', 'basil'];
const selected = ref<string[]>([]);

const all = useBehavior(checkbox, () => ({
  labelElementType: 'span',
  isSelected: selected.value.length === toppings.length,
  isIndeterminate:
    selected.value.length > 0 && selected.value.length < toppings.length,
  onSelectedChange: (isSelected) =>
    (selected.value = isSelected ? [...toppings] : []),
}));
</script>

<template>
  <label v-bind="all.parts.root">
    <input v-bind="all.parts.input" />
    <span v-bind="all.parts.indicator" />
    <span v-bind="all.parts.label">All toppings</span>
  </label>
</template>
```

```js
function update() {
  const count = rows.filter((row) => row.isSelected).length;
  all.isSelected = count === rows.length;
  all.isIndeterminate = count > 0 && count < rows.length;
}
all.addEventListener('selectedchange', (event) => {
  for (const row of rows) row.isSelected = event.detail;
  update();
});
```

The `indeterminate` attribute on `<uni-checkbox>` sets it from markup. The
demo above has the whole script.

For a list of checkboxes with one value, use a
[CheckboxGroup](/components/checkbox-group/).

### Controlled

Pass `isSelected` and the checkbox shows that value. A click still toggles the
input natively and fires `selectedchange` with the new state; if you don't
pass it back by the end of that event (a React state update or a Vue ref set
in the callback counts), the checkbox goes back to `isSelected`. On the element, set the `isSelected`
property, and set it again in a `selectedchange` listener to accept a change.
See [State](/guide/state/).

### In a form

* **Submission.** The input is a native checkbox: while checked, the form
  submits its `name` with its `value` (`on` when it has none). You write
  both; the `value` prop writes the input's `value` for you, which a
  [CheckboxGroup](/components/checkbox-group/) needs.
* **Reset.** A form reset returns an uncontrolled checkbox to
  `defaultSelected`, even one set after it rendered; a controlled one gets a
  `selectedchange` asking for `defaultSelected`, or without one the state it
  mounted with. It leaves `indeterminate` alone, as the browser does.
* **Disabled.** A disabled checkbox is never submitted, and neither is one
  disabled with `isFocusableWhenDisabled`. A read-only one is submitted.
* **Validation.** In a [Form](/components/form/), the form holds the selected
  state under its `name` and checks it on each change: `isRequired` fails
  while it is off. `isRequired` writes `aria-required`, not the native
  `required`, so the browser shows no message of its own. Without a form,
  you decide when it is invalid and set `isInvalid`.

See [Forms](/guide/forms/).

## Anatomy

| Part          | Element                                                    | Notes                                                                                                                                                                                                                                                                              |
| ------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `root`        | Any; usually `div`                                         | Optional. The outermost element, around the wrapper, the description and the error. No role. Without a wrapper, a `label` root makes its whole area toggle the checkbox.                                                                                                           |
| `wrapper`     | Any; a `label` makes its whole area toggle the checkbox    | Optional. The area around the input, the indicator and the label. No role.                                                                                                                                                                                                         |
| `input`       | `input`                                                    | Required. UniAria writes `type="checkbox"`, its `id` (unless it has one) and ARIA attributes, `disabled`, and the `checked` and `indeterminate` properties. You own `name`, `value` (unless the `value` prop is set), `form`, `autofocus`, `tabindex` and the `checked` attribute. |
| `indicator`   | Any; usually `span`                                        | Optional. The box you draw in place of the input, with the check or the dash. Gets `aria-hidden="true"`.                                                                                                                                                                           |
| `label`       | `label` by default, any element through `labelElementType` | Names the checkbox. A `span` inside a `label` wrapper or root.                                                                                                                                                                                                                     |
| `description` | Any                                                        | Optional. Outside a `label` wrapper or root.                                                                                                                                                                                                                                       |
| `error`       | Any                                                        | Optional. Outside a `label` wrapper or root. Hidden unless invalid.                                                                                                                                                                                                                |
| `errorText`   | any, inside `error`                                        | Optional. In a [Form](/components/form/), UniAria writes the first error's text here; leave it out to render errors yourself.                                                                                                                                                      |

A part keeps an `id` you give it, and everything that refers to the part
follows it; UniAria writes an `id` only on a part that has none (see
[Ids](/guide/accessibility/#ids)).

You own every part's content. UniAria fills none of them.

## Props

| Prop                      | Attribute                 | Type       | Default   | Description                                                                                                                  |
| ------------------------- | ------------------------- | ---------- | --------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `isSelected`              | — (property)              | `boolean`  | —         | The state, controlled. Pair it with `onSelectedChange`.                                                                      |
| `defaultSelected`         | `default-selected`        | `boolean`  | `false`   | The starting state, uncontrolled. The element adopts the input's `checked`.                                                  |
| `isIndeterminate`         | `indeterminate`           | `boolean`  | `false`   | Shows the dash. See Indeterminate.                                                                                           |
| `isDisabled`              | `disabled`                | `boolean`  | `false`   | Native `disabled`: not focusable, never toggles, not submitted. Hides the error.                                             |
| `isFocusableWhenDisabled` | `focusable-when-disabled` | `boolean`  | `false`   | While disabled, `aria-disabled` instead of `disabled`: focusable, so a tooltip can explain it, but never toggles or submits. |
| `isReadOnly`              | `readonly`                | `boolean`  | `false`   | `aria-readonly="true"`, and every toggle is cancelled. Focusable and submitted.                                              |
| `isRequired`              | `required`                | `boolean`  | `false`   | `aria-required="true"`, not the native `required`.                                                                           |
| `isInvalid`               | `invalid`                 | `boolean`  | —         | `aria-invalid="true"`, and shows and announces the error. Unset in a [Form](/components/form/), the form's errors decide it. |
| `value`                   | `value`                   | `string`   | —         | The checkbox's key in a CheckboxGroup's value, written to the input's `value`. Required in a group.                          |
| `owner`                   | `group` (an id)           | `Instance` | —         | The [CheckboxGroup](/components/checkbox-group/) of a checkbox that isn't inside it.                                         |
| `inputId`                 | —                         | `string`   | generated | The input's `id` while the input has none of its own.                                                                        |
| `labelElementType`        | — (read from the tag)     | `string`   | `'label'` | The label part's tag. `'span'` inside a `label` wrapper or root; only a `label` gets `for`.                                  |
| `labelledBy`              | `labelled-by`             | `string`   | —         | More ids for `aria-labelledby`, after the label part's.                                                                      |
| `describedBy`             | `described-by`            | `string`   | —         | More ids for `aria-describedby`, after the description's and the error's.                                                    |

In a [Form](/components/form/), Checkbox also takes `name`, `form`, `validate`,
`validationTrigger`, `validationDelay`, `validationMessages`, `dependsOn`,
`isEqual` and `shouldPreserve`: see [Field props for the
form](/components/form/#field-props-for-the-form).

## Events

| Event            | Callback prop      | Detail    | When                                                                                                    |
| ---------------- | ------------------ | --------- | ------------------------------------------------------------------------------------------------------- |
| `selectedchange` | `onSelectedChange` | `boolean` | The user toggled the checkbox (right before the native `change`), or a form reset asks for its default. |
| `focuschange`    | `onFocusChange`    | `boolean` | The input gained or lost focus.                                                                         |

`selectedchange` never fires for a value you set. A checkbox in a group
reports through the group's `valuechange` instead. The custom element
dispatches each event on itself, bubbling, with the value in `detail`. In
React, UniAria's part props include the input's `onInput`, `onClick`,
`onFocus` and `onBlur`: use the events above rather than replacing them. In
Vue, a listener of your own runs beside UniAria's.

## State

| Field             | Type      | Meaning                                                                                        |
| ----------------- | --------- | ---------------------------------------------------------------------------------------------- |
| `isSelected`      | `boolean` | The current state: the controlled value, the uncontrolled one, or the group's.                 |
| `isIndeterminate` | `boolean` | The `isIndeterminate` prop.                                                                    |
| `isFocused`       | `boolean` | The input has focus.                                                                           |
| `isFocusVisible`  | `boolean` | The input has focus from a keyboard or assistive technology.                                   |
| `isHovered`       | `boolean` | A mouse or pen is over the wrapper (without one, the root) or the input. Never while disabled. |
| `isDisabled`      | `boolean` | Disabled, by the prop or the group.                                                            |
| `isReadOnly`      | `boolean` | Read-only, by the prop or the group.                                                           |
| `isRequired`      | `boolean` | Required, by the prop or the group.                                                            |
| `isInvalid`       | `boolean` | Invalid, by the prop or the group.                                                             |
| `isErrorShown`    | `boolean` | The error part is shown: invalid and not disabled.                                             |

To render something from state in React or Vue, select it, and the component
renders again only when that value changes (Vue gives a ref):

```ts
const isAccepted = useBehaviorState(terms, (state) => state.isSelected);
```

On the custom element, read `element.instance.getState()`. The `isSelected`
property holds only the value you set, not the current state. See
[State](/guide/state/).

## Styling

Every part carries the state attributes, so the box, the label and the
messages can be styled without reading state.

| Attribute               | When                                                                     |
| ----------------------- | ------------------------------------------------------------------------ |
| `data-uni="checkbox"`   | On the root, always.                                                     |
| `data-element="<Part>"` | On every other part, always.                                             |
| `data-selected`         | The checkbox is selected.                                                |
| `data-indeterminate`    | `isIndeterminate` is true.                                               |
| `data-hovered`          | A mouse or pen is over the wrapper (without one, the root) or the input. |
| `data-focused`          | The input has focus.                                                     |
| `data-focus-visible`    | The input has focus from a keyboard or a screen reader.                  |
| `data-disabled`         | The checkbox is disabled.                                                |
| `data-readonly`         | The checkbox is read-only.                                               |
| `data-invalid`          | The checkbox is invalid.                                                 |
| `data-required`         | The checkbox is required.                                                |

A checkbox can be both selected and indeterminate: put the dash's rules after
the check's so the dash wins. State attributes are present or absent, never
`"false"`. There is no pressed state; use `:active`. UniAria writes no styles.

### Default styles

The optional [`@uniaria/styles`](/guide/default-styles/) package styles
every part, as in the demo above, whose CSS tab holds only the page's layout. It
lays the input over the wrapper, or over a `label` root, invisible and
clickable, and draws the check and the dash on the indicator's `::before`.

## Keyboard

| Key   | Behavior                                                                                        |
| ----- | ----------------------------------------------------------------------------------------------- |
| Space | Toggles, natively. Nothing when read-only or focusable while disabled.                          |
| Enter | Nothing on the checkbox: it submits the form, natively.                                         |
| Tab   | Moves focus, natively. A disabled checkbox is skipped, unless `isFocusableWhenDisabled` is set. |

Checkbox handles no keys itself.

## Accessibility

* **Native state.** Screen readers read the input's own role and `checked`
  and `indeterminate` state ("mixed"), and announce a toggle themselves, so
  UniAria never writes `aria-checked` and announces nothing on a toggle.
* **Spoken output.** "checkbox, Accept terms, not checked", then "checked".
  The description follows the name; "read only", "required", "invalid" and
  "disabled" follow the state. An invalid checkbox reads its error as its
  description.
* **Labelling.** Give every checkbox a label part, or an `aria-label` on the
  input. A link inside the `label` wrapper or root ("I accept the terms")
  keeps its own click.
* **Focus.** A press on the input, or on a `label` that activates it, focuses
  the input in every browser, Safari included, which doesn't on its own.
  Focus from a pointer isn't visible: `data-focus-visible` stays off, so draw
  the ring on it. Keys and scripts move no focus, and neither does a press on
  a link inside the label.
* **Disabled with a reason.** Natively disabled checkboxes can't be reached
  by keyboard. To explain one with a [Tooltip](/components/tooltip/), set
  `isFocusableWhenDisabled` and link the tooltip through the `tooltip` prop
  (React and Vue) or attribute (an id).

## Compared with other libraries

* **A native input.** The APG examples build `role="checkbox"` with
  `aria-checked`; UniAria keeps the native checkbox, as React Aria does.
* **`selectedchange`** is React Aria's `onChange`, with the same
  `isSelected`, `defaultSelected` and `isIndeterminate`.
* **Read-only cancels the click**, as in Zag; React Aria lets the toggle
  happen and reverts it. No `input` or `change` event fires.
* **A press focuses the input,** on the input or its label, as React Aria and
  Zag do on a label press, so focus is the same in every browser.
* **The `checked` attribute stays the default.** Zag reflects the state into
  it, which leaves a native form reset nothing to restore.
* **No validation.** React Aria can validate natively; UniAria shows
  `isInvalid` and leaves the decision to your form.
