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 a toggleable on/off control with an optional third "indeterminate" state. It owns the checked state, the toggle logic and its indicator’s reveal and exit motion — settings toggles, opt-ins, multi-select lists, “select all” headers. For a plain boolean flip with a sliding handle, Switch fits better.

Preview

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, so nothing is drawn until you give it a size and colours.

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 needs to read or set the value — persisting a setting, syncing with a server, 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 onto your own button and give the indicator a glyph. The root never touches your box’s colour, so branch on the checked state yourself. To animate the indicator in, pass a transition from createIndicatorRevealRecipe at its 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 tri-state pattern: a header checkbox derived from a set of row checkboxes. It shows "indeterminate" while only some rows are checked, and toggling from that state resolves to true.

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 confirmation checkbox that must be checked before a costly action goes through.

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, setChecked calls from context are dropped, and the button leaves gamepad selection.

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. The state is a CheckedState: true, false or "indeterminate". It covers the tri-state “select all” pattern as well as a plain toggle.

Activation toggles from "indeterminate" to true, otherwise flipping the boolean. Set the indeterminate state yourself through controlled checked — the toggle never produces it.

Activation and selection

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

With asChild the toggle merges onto your child element through Slot: the slot’s Active, Selectable and ref win, and handlers compose. Use a textbutton or imagebutton so Activated fires.

Root color is yours

The root’s BackgroundColor3 is never written by the primitive. Branch on the checked state you already have, adding a response motion of your own if you want the change eased.

Disabled and required

disabled blocks both the toggle and direct state changes, and removes the button from gamepad selection. required is exposed on context for your own validation wiring and changes no interaction behaviour.

Indicator presence and motion

Checkbox.Indicator is presence-driven by the checked state, present whenever checked is not false. It mounts when checked and unmounts when unchecked, holding through any exit transition, and runs no motion of its own.

To animate it, pass createIndicatorRevealRecipe(size) as transition, built with the size your indicator settles at — it grows from zero to that size while fading in. forceMount keeps the indicator mounted while unchecked and through its exit. With asChild, your child element replaces the frame and its Visible 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.