# Checkbox

> Checkbox with an optional label, controlled or uncontrolled, an indeterminate (mixed) state and a haptic tick.

Component · forms. Registry item `checkbox`, shown in the vega Style. JSON: https://nativecn.dev/r/styles/vega/checkbox.json

## Installation

```sh
npx nativecn-cli@latest add checkbox
```

Also installs: [theme](https://nativecn.dev/docs/theme/theme), [pressable](https://nativecn.dev/docs/primitives/pressable), [form-field-context](https://nativecn.dev/docs/primitives/form-field-context), [text](https://nativecn.dev/docs/components/text), [icon](https://nativecn.dev/docs/components/icon), [use-controllable-state](https://nativecn.dev/docs/primitives/use-controllable-state).

Packages: `lucide-react-native@^1.51.0`, `react-native-svg`.

## Usage

### Checkbox demo

A labelled Checkbox, a 'select all' with an indeterminate state, and a disabled one.

```tsx
import { useState } from "react";
import { View } from "react-native";

import { Checkbox } from "@/components/checkbox";
import { createStyles } from "@/theme";

const useStyles = createStyles((t) => ({
  root: { gap: t.spacing[4] },
  nested: { gap: t.spacing[3], paddingLeft: t.spacing[6] },
}));

const TOPPINGS = ["Cheese", "Olives", "Basil"];

export default function CheckboxDemo() {
  const styles = useStyles();
  const [picked, setPicked] = useState<string[]>(["Cheese"]);
  const all = picked.length === TOPPINGS.length;

  return (
    <View style={styles.root}>
      <Checkbox label="Accept the terms" defaultChecked />
      <Checkbox
        label="All toppings"
        checked={all}
        indeterminate={picked.length > 0 && !all}
        onCheckedChange={(checked) => setPicked(checked ? TOPPINGS : [])}
      />
      <View style={styles.nested}>
        {TOPPINGS.map((topping) => (
          <Checkbox
            key={topping}
            label={topping}
            checked={picked.includes(topping)}
            onCheckedChange={(checked) =>
              setPicked((prev) =>
                checked ? [...prev, topping] : prev.filter((p) => p !== topping),
              )
            }
          />
        ))}
      </View>
      <Checkbox label="Unavailable" disabled />
    </View>
  );
}
```

## Props

| Prop | Type and description |
|---|---|
| `checked` | boolean: the controlled checked state |
| `defaultChecked` | boolean: the starting state while uncontrolled (default false) |
| `onCheckedChange` | (checked: boolean) => void: called when the user toggles it |
| `label` | string: visible text next to the box, also the accessible name |
| `indeterminate` | boolean: shows a dash and is announced as mixed; pressing it checks the box |
| `disabled` | boolean: dimmed, announced as disabled, no handler runs |
| `style` | layout only, merged last onto the root |

## Accessibility

- role checkbox with aria-checked true, false or mixed.
- The label is the accessible name; without a label, pass aria-label.
- The whole row (box and label) is the tap target, at least 48.
- disabled is announced as disabled and no handler runs.
- Inside a FormField the field's label and error become its name and hint.

## Notes

A light haptic tick on toggle when config.haptics is on. Inside a FormField it takes the field's disabled, and an error outlines the box in destructive. The box uses the checkbox.box Style Slot and the pressed look checkbox.pressed.

## Source

### {components}/checkbox.tsx

```tsx
import { Check, Minus } from "lucide-react-native";
import { View, type GestureResponderEvent, type StyleProp, type ViewStyle } from "react-native";

import { Icon } from "@/components/icon";
import { useFormField } from "@/components/primitives/form-field-context";
import { Pressable, type PressableProps } from "@/components/primitives/pressable";
import { Text } from "@/components/text";
import { useControllableState } from "@/hooks/use-controllable-state";
import { createStyles } from "@/theme";

export type CheckboxProps = Omit<
  PressableProps,
  "children" | "style" | "size" | "pressedStyle" | "haptic" | "loading" | "role"
> & {
  /** Controlled checked state. */
  checked?: boolean;
  /** Starting checked state while uncontrolled (default `false`). */
  defaultChecked?: boolean;
  /** Called with the next checked state when the user toggles it. */
  onCheckedChange?: (checked: boolean) => void;
  /** Visible text next to the box; it is also the accessible name. */
  label?: string;
  /**
   * Shows a dash and is announced as "mixed" (e.g. a "select all" with some items checked).
   * Pressing it checks the box; the screen decides when to clear it.
   */
  indeterminate?: boolean;
  /** Layout only, merged last onto the root. */
  style?: StyleProp<ViewStyle>;
};

const useStyles = createStyles((t) => ({
  root: {
    flexDirection: "row",
    alignItems: "center",
    gap: t.spacing[2],
    alignSelf: "flex-start",
  },
  box: {
    ...{
    width: t.scaleValue(20),
    height: t.scaleValue(20),
    borderRadius: t.radius.sm,
  },
    alignItems: "center",
    justifyContent: "center",
    borderCurve: "continuous",
    borderWidth: t.borderWidth.default,
    borderColor: t.colors.input,
    backgroundColor: t.colors.background,
  },
  boxOn: { backgroundColor: t.colors.primary, borderColor: t.colors.primary },
  boxError: { borderColor: t.colors.destructive },
  pressed: { opacity: 0.7 },
  disabled: { opacity: t.opacity.disabled },
}));

/**
 * A checkbox with an optional label. Works controlled (`checked` + `onCheckedChange`) or
 * uncontrolled (`defaultChecked`); `indeterminate` shows a dash and is announced as mixed.
 * Built on the Pressable Primitive: the whole row is the tap target (at least 48), it gives a
 * light haptic tick when `config.haptics` is on, and nothing fires while disabled. Inside a
 * FormField it reads the field's label and error, takes its `disabled`, and an error outlines
 * the box in `destructive`.
 */
export function Checkbox({
  checked: checkedProp,
  defaultChecked = false,
  onCheckedChange,
  label,
  indeterminate = false,
  disabled: disabledProp,
  onPress,
  style,
  ...props
}: CheckboxProps) {
  const styles = useStyles();
  // Inside a FormField: its label and error become the name and hint, and its `disabled` applies.
  const field = useFormField();
  const disabled = disabledProp ?? field?.disabled ?? false;
  const [checked, setChecked] = useControllableState({
    value: checkedProp,
    defaultValue: defaultChecked,
    onChange: onCheckedChange,
  });
  const on = checked || indeterminate;

  const handlePress = (event: GestureResponderEvent) => {
    // An indeterminate box always becomes checked.
    setChecked(indeterminate ? true : !checked);
    onPress?.(event);
  };

  const box = styles.box;
  return (
    <Pressable
      role="checkbox"
      aria-checked={indeterminate ? "mixed" : checked}
      disabled={disabled}
      haptic="selection"
      // Without a label the box is the whole target, so its size is known before layout.
      size={label == null ? { width: box.width, height: box.height } : undefined}
      pressedStyle={styles.pressed}
      onPress={handlePress}
      style={[styles.root, disabled ? styles.disabled : null, style]}
      {...field?.accessibilityProps}
      {...props}
    >
      <View style={[box, on ? styles.boxOn : field?.status === "error" ? styles.boxError : null]}>
        {on ? (
          <Icon
            icon={indeterminate ? Minus : Check}
            size="sm"
            color="primaryForeground"
            strokeWidth={3}
          />
        ) : null}
      </View>
      {label != null ? <Text variant="label">{label}</Text> : null}
    </Pressable>
  );
}
```
