Latticecomponents

Checkbox

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

@lattice-ui/react-checkboxStable directionimport Checkboxdepends 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 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.

Edit

Import

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.

Checkbox anatomy
<Checkbox.Root>
<Checkbox.Indicator />
</Checkbox.Root>
PartRequiredResponsibility
Checkbox.RootyesOwns the checked state, handles toggling, and renders the activatable button.
Checkbox.IndicatornoMounts 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.

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.

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.

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).

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.

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.

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 CheckedStatetrue, 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.

API reference

Checkbox.Root

PropTypeDescription
checkedboolean | "indeterminate"Controlled checked state. Pair with onCheckedChange.
defaultCheckedboolean | "indeterminate"Initial checked state for uncontrolled usage. Defaults to false.
onCheckedChange(checked: boolean | "indeterminate") => voidCalled whenever the checked state changes.
disabledbooleanPrevents toggling and removes the button from gamepad selection. Defaults to false.
requiredbooleanMarks the checkbox as required for your own form/validation wiring. Exposed on context; does not change interaction on its own. Defaults to false.
asChildbooleanMerge checkbox behavior onto the single child element instead of the textbutton the part renders. The child must be an activatable button.
childrenReact.ReactNodeThe checkbox contents, typically a Checkbox.Indicator. Must be a single valid element when asChild is set.

Checkbox.Indicator

PropTypeDescription
transitionPresenceMotionConfigReveal/exit motion. None by default; pass createIndicatorRevealRecipe(size) built at the size your indicator settles at.
forceMountbooleanKeeps the indicator mounted while unchecked and through its exit motion, instead of unmounting when unchecked. Defaults to false.
asChildbooleanMerge indicator behavior onto the single child element instead of the frame the part renders. The child's Visible property is bound to presence.
childrenReact.ReactNodeThe indicator contents shown while checked, such as a checkmark glyph.