# Checkbox

> A checked-state primitive with indeterminate support, controlled or uncontrolled state, and an optional presence-animated indicator.

Source: https://docs.astra-void.xyz/lattice-ui/components/checkbox/

`@lattice-ui/react-checkbox` · Stable direction · import `Checkbox` · depends on `runtime`, `layer`, `motion`

Checkbox is the primitive for a toggleable on/off control with an optional third `"indeterminate"` state. It owns the checked state, the toggle logic, and the reveal/exit motion of its indicator, so your component only has to render the box and what goes inside it.

Reach for Checkbox for settings toggles, opt-ins, multi-select lists, and "select all" headers — anywhere you need a tri-state value (checked, unchecked, or indeterminate) with controlled or uncontrolled state and a built-in animated indicator. For a plain boolean flip with a sliding handle, [Switch](https://docs.astra-void.xyz/lattice-ui/components/switch.md) is usually the better fit.

## Preview

The component running live — the same `@rbxts/react` tree Roblox renders, mounted in the browser. Click the boxes to toggle them.

_Interactive preview._

## Import

```ts
import { Checkbox } from "@lattice-ui/react-checkbox";
```

## Anatomy

Compose `Root` as the toggleable button and place an `Indicator` inside it to show the checked state.

```tsx title="Checkbox anatomy"
<Checkbox.Root>
  <Checkbox.Indicator />
</Checkbox.Root>
```

| Part | Required | Responsibility |
| --- | --- | --- |
| `Checkbox.Root` | yes | Owns the checked state, handles toggling, and renders the activatable button. |
| `Checkbox.Indicator` | no | Mounts while the box is checked or indeterminate. Animates if given a `transition`. |

## Examples

### Basic usage

Uncontrolled state seeded with `defaultChecked`. The root renders an unstyled `textbutton` with no label of its own, so give it a size and colors — the state below is wired up correctly either way, but nothing is drawn until you say what it should look like.

```tsx title="BasicCheckbox.tsx"
import { Checkbox } from "@lattice-ui/react-checkbox";

export function BasicCheckbox() {
  return (
    <Checkbox.Root
      defaultChecked={true}
      onCheckedChange={(checked) => print(`checkbox is now: ${checked}`)}
    >
      <Checkbox.Indicator />
    </Checkbox.Root>
  );
}
```

### Controlled state

Pass `checked` and `onCheckedChange` when something outside the checkbox needs to read or set the value — persisting a setting, syncing with a server, or resetting from elsewhere.

```tsx title="RememberMeCheckbox.tsx"
import { useState } from "@rbxts/react";
import { Checkbox } from "@lattice-ui/react-checkbox";

export function RememberMeCheckbox() {
  const [checked, setChecked] = useState<boolean | "indeterminate">(false);

  return (
    <Checkbox.Root checked={checked} onCheckedChange={setChecked}>
      <Checkbox.Indicator />
    </Checkbox.Root>
  );
}
```

### Custom box with asChild

Use `asChild` to project the toggle behavior onto your own button, and give the indicator a glyph. The root no longer touches your box's color, so branch on the checked state yourself. If you want the indicator to animate in, pass a `transition` built with `createIndicatorRevealRecipe` at your indicator's actual size.

```tsx title="StyledCheckbox.tsx"
import { useState } from "@rbxts/react";
import { Checkbox } from "@lattice-ui/react-checkbox";
import { createIndicatorRevealRecipe } from "@lattice-ui/react-motion";

const INDICATOR_REVEAL = createIndicatorRevealRecipe(UDim2.fromOffset(16, 16));

export function StyledCheckbox() {
  const [checked, setChecked] = useState<boolean | "indeterminate">(false);

  return (
    <Checkbox.Root checked={checked} onCheckedChange={setChecked} asChild>
      <textbutton AutoButtonColor={false} Size={UDim2.fromOffset(24, 24)} Text="">
        <uicorner CornerRadius={new UDim(0, 6)} />
        <uistroke Color={Color3.fromRGB(88, 142, 255)} Thickness={1} />

        <Checkbox.Indicator transition={INDICATOR_REVEAL} asChild>
          <imagelabel
            AnchorPoint={new Vector2(0.5, 0.5)}
            BackgroundTransparency={1}
            Image="rbxassetid://1234567890"
            Position={UDim2.fromScale(0.5, 0.5)}
            Size={UDim2.fromOffset(16, 16)}
          />
        </Checkbox.Indicator>
      </textbutton>
    </Checkbox.Root>
  );
}
```

### Select all with indeterminate

The classic tri-state pattern: a header checkbox derived from a set of row checkboxes. The header shows `"indeterminate"` while only some rows are checked; toggling it from that state resolves to `true` (checking everything).

```tsx title="PartyInviteList.tsx"
import { useState } from "@rbxts/react";
import { Checkbox } from "@lattice-ui/react-checkbox";

const MEMBERS = ["Aria", "Bolt", "Cinder"];

export function PartyInviteList() {
  const [invited, setInvited] = useState<Record<string, boolean>>({});

  const invitedCount = MEMBERS.filter((member) => invited[member] === true).size();
  const allChecked: boolean | "indeterminate" =
    invitedCount === MEMBERS.size() ? true : invitedCount === 0 ? false : "indeterminate";

  const setAll = (checked: boolean | "indeterminate") => {
    const nextInvited: Record<string, boolean> = {};
    for (const member of MEMBERS) {
      nextInvited[member] = checked === true;
    }
    setInvited(nextInvited);
  };

  return (
    <frame BackgroundTransparency={1} Size={UDim2.fromOffset(220, 160)}>
      <uilistlayout Padding={new UDim(0, 8)} SortOrder={Enum.SortOrder.LayoutOrder} />

      <Checkbox.Root checked={allChecked} onCheckedChange={setAll}>
        <Checkbox.Indicator />
      </Checkbox.Root>

      {MEMBERS.map((member) => (
        <frame BackgroundTransparency={1} Size={UDim2.fromOffset(220, 24)} key={member}>
          <uilistlayout
            FillDirection={Enum.FillDirection.Horizontal}
            Padding={new UDim(0, 8)}
            VerticalAlignment={Enum.VerticalAlignment.Center}
          />
          <Checkbox.Root
            checked={invited[member] === true}
            onCheckedChange={(checked) => setInvited({ ...invited, [member]: checked === true })}
            asChild
          >
            <textbutton AutoButtonColor={false} Size={UDim2.fromOffset(20, 20)} Text="">
              <uicorner CornerRadius={new UDim(0, 4)} />
              <Checkbox.Indicator asChild>
                <frame
                  AnchorPoint={new Vector2(0.5, 0.5)}
                  BackgroundColor3={Color3.fromRGB(240, 244, 252)}
                  BorderSizePixel={0}
                  Position={UDim2.fromScale(0.5, 0.5)}
                  Size={UDim2.fromOffset(12, 12)}
                />
              </Checkbox.Indicator>
            </textbutton>
          </Checkbox.Root>
          <textlabel
            BackgroundTransparency={1}
            Size={UDim2.fromOffset(160, 20)}
            Text={member}
            TextColor3={Color3.fromRGB(236, 241, 249)}
            TextXAlignment={Enum.TextXAlignment.Left}
          />
        </frame>
      ))}
    </frame>
  );
}
```

### Gating an action

A common game-UI pattern: a confirmation checkbox that must be checked before a destructive or costly action goes through, such as confirming a trade.

```tsx title="TradeConfirmation.tsx"
import { useState } from "@rbxts/react";
import { Checkbox } from "@lattice-ui/react-checkbox";

export function TradeConfirmation(props: { onConfirm: () => void }) {
  const [accepted, setAccepted] = useState<boolean | "indeterminate">(false);

  return (
    <frame BackgroundColor3={Color3.fromRGB(24, 26, 32)} Size={UDim2.fromOffset(280, 120)}>
      <uilistlayout Padding={new UDim(0, 10)} SortOrder={Enum.SortOrder.LayoutOrder} />
      <uipadding PaddingLeft={new UDim(0, 12)} PaddingTop={new UDim(0, 12)} />

      <textlabel
        BackgroundTransparency={1}
        Size={UDim2.fromOffset(256, 20)}
        Text="I understand this trade cannot be undone"
        TextColor3={Color3.fromRGB(236, 241, 249)}
        TextXAlignment={Enum.TextXAlignment.Left}
      />

      <Checkbox.Root checked={accepted} onCheckedChange={setAccepted}>
        <Checkbox.Indicator />
      </Checkbox.Root>

      <textbutton
        Active={accepted === true}
        AutoButtonColor={accepted === true}
        BackgroundColor3={
          accepted === true ? Color3.fromRGB(88, 142, 255) : Color3.fromRGB(59, 66, 84)
        }
        Event={{
          Activated: () => {
            if (accepted === true) {
              props.onConfirm();
            }
          },
        }}
        Size={UDim2.fromOffset(120, 32)}
        Text="Confirm trade"
        TextColor3={Color3.fromRGB(240, 244, 252)}
      />
    </frame>
  );
}
```

### Disabled state

`disabled` blocks toggling entirely — activation is ignored and `setChecked` calls from context are dropped — and removes the button from gamepad selection. Here a premium-only option stays visible but inert.

```tsx title="PremiumOption.tsx"
import { Checkbox } from "@lattice-ui/react-checkbox";

export function PremiumOption(props: { hasPremium: boolean }) {
  return (
    <Checkbox.Root defaultChecked={false} disabled={!props.hasPremium}>
      <Checkbox.Indicator />
    </Checkbox.Root>
  );
}
```

## How it behaves

### Checked state

`Checkbox.Root` is controllable on `checked`/`onCheckedChange`, with `defaultChecked` for uncontrolled usage (defaulting to `false`). The state is a `CheckedState` — `true`, `false`, or `"indeterminate"` — so it supports the tri-state "select all" pattern as well as a plain boolean toggle.

Activating the checkbox runs a toggle: from `"indeterminate"` it goes to `true`, otherwise it flips the boolean. To set the indeterminate state, drive it yourself through controlled `checked` (typically computed from child selections); the toggle never produces `"indeterminate"` on its own.

### Activation and selection

`Checkbox.Root` renders an unstyled activatable `textbutton` that toggles on `Activated`, so click, tap, and gamepad activation all work. Give it a size and colors. It is `Active` and `Selectable` only while enabled, so a disabled checkbox drops out of gamepad selection.

With `asChild`, the toggle behavior is merged onto your single child element through the shared `Slot`: the slot's `Active`, `Selectable`, and ref win over the child's own props, and event handlers compose (both the slot's `Activated` toggle and any handler you pass on the child run). Use a `textbutton` or `imagebutton` so `Activated` fires.

### Root color is yours

The root's `BackgroundColor3` is entirely yours as of 0.7.0. It used to animate between a fixed accent palette with no opt-out; now it is never written by the primitive. Branch on the checked state you already have, and add a response motion of your own if you want the change to ease rather than snap.

### Disabled and required

`disabled` (default `false`) blocks both the toggle and direct state changes, and the button removes itself from gamepad selection while disabled. `required` (default `false`) is exposed on context for your own form/validation wiring and submission logic; it does not change interaction behavior on its own.

### Indicator presence and motion

`Checkbox.Indicator` is presence-driven by the checked state — it is present whenever `checked` is not `false` (so for both `true` and `"indeterminate"`). It mounts when checked and unmounts when unchecked, holding through any exit transition first. It runs no motion of its own.

To animate it, pass `createIndicatorRevealRecipe(size)` from `@lattice-ui/react-motion` as `transition`, built with the size your indicator actually settles at — the recipe grows from zero to that size while fading in. Pass `forceMount` to keep the indicator mounted while unchecked and through its exit, bypassing the presence wrapper so you can drive visibility yourself. With `asChild`, your child element is rendered in place of the frame the part renders and its `Visible` property is bound to the presence state.

> **Indeterminate is controlled-only**
>
> Toggling never yields `"indeterminate"` — it maps indeterminate to `true` and otherwise flips the boolean. Provide `"indeterminate"` through the controlled `checked` prop, usually derived from a group of child checkboxes, and resolve it to `true`/`false` in your `onCheckedChange` handler.

> **The primitive owns the box color**
>
> Before 0.7.0 the root drove your element's `BackgroundColor3` between fixed checked and unchecked accents, with no way to opt out. It no longer writes color at all — in either mode — so a background you set stays exactly as you set it.

> **Indicator shows for any non-false state**
>
> The indicator is visible for both `true` and `"indeterminate"`. If you need a different glyph for the indeterminate state, branch on the checked value inside your indicator's children rather than mounting a second indicator.

## API reference

### Checkbox.Root

| Prop | Type | Description |
| --- | --- | --- |
| `checked` | `boolean \| "indeterminate"` | Controlled checked state. Pair with onCheckedChange. |
| `defaultChecked` | `boolean \| "indeterminate"` | Initial checked state for uncontrolled usage. Defaults to false. |
| `onCheckedChange` | `(checked: boolean \| "indeterminate") => void` | Called whenever the checked state changes. |
| `disabled` | `boolean` | Prevents toggling and removes the button from gamepad selection. Defaults to false. |
| `required` | `boolean` | Marks the checkbox as required for your own form/validation wiring. Exposed on context; does not change interaction on its own. Defaults to false. |
| `asChild` | `boolean` | Merge checkbox behavior onto the single child element instead of the textbutton the part renders. The child must be an activatable button. |
| `children` | `React.ReactNode` | The checkbox contents, typically a Checkbox.Indicator. Must be a single valid element when asChild is set. |

### Checkbox.Indicator

| Prop | Type | Description |
| --- | --- | --- |
| `transition` | `PresenceMotionConfig` | Reveal/exit motion. None by default; pass createIndicatorRevealRecipe(size) built at the size your indicator settles at. |
| `forceMount` | `boolean` | Keeps the indicator mounted while unchecked and through its exit motion, instead of unmounting when unchecked. Defaults to false. |
| `asChild` | `boolean` | Merge indicator behavior onto the single child element instead of the frame the part renders. The child's Visible property is bound to presence. |
| `children` | `React.ReactNode` | The indicator contents shown while checked, such as a checkmark glyph. |

## Related

- [Controlled state](https://docs.astra-void.xyz/lattice-ui/guides/controlled-state.md)
- [Presence and motion](https://docs.astra-void.xyz/lattice-ui/guides/presence-and-motion.md)
- [asChild composition](https://docs.astra-void.xyz/lattice-ui/guides/as-child-composition.md)
- [Roblox UI constraints](https://docs.astra-void.xyz/lattice-ui/guides/roblox-ui-constraints.md)
