@lattice-ui/react-checkboxStable directionimport Checkboxdepends on runtime, layer, motionCheckbox 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.
import type { CheckedState } from "@lattice-ui/react-checkbox";import { Checkbox } from "@lattice-ui/react-checkbox";import { React } from "@lattice-ui/react-runtime";import { mergeGuiProps, Text, useTheme } from "@lattice-ui/react-style";import { buttonRecipe } from "../../../playground/src/client/theme/recipes";
type PreferenceRow = { label: string; description: string; checked: boolean; disabled?: boolean; onChange?: (state: CheckedState) => void;};
function CheckboxExample() { const { theme } = useTheme(); const [updates, setUpdates] = React.useState(true); const [marketing, setMarketing] = React.useState(false);
const rows: Array<PreferenceRow> = [ { label: "Product updates", description: "Feature news and improvements.", checked: updates, onChange: (state) => setUpdates(state === true), }, { label: "Marketing", description: "Tips, offers, and event invites.", checked: marketing, onChange: (state) => setMarketing(state === true), }, { label: "Security alerts", description: "Important notices about your account.", checked: true, disabled: true, }, ];
return ( <frame BackgroundColor3={theme.colors.surfaceElevated} BorderSizePixel={0} Size={UDim2.fromScale(1, 1)}> <uicorner CornerRadius={new UDim(0, theme.radius.lg)} /> <uistroke Color={theme.colors.border} Thickness={1} /> <uipadding PaddingBottom={new UDim(0, theme.space[16])} PaddingLeft={new UDim(0, theme.space[20])} PaddingRight={new UDim(0, theme.space[20])} PaddingTop={new UDim(0, theme.space[16])} /> <uilistlayout FillDirection={Enum.FillDirection.Vertical} Padding={new UDim(0, theme.space[10])} />
<frame BackgroundTransparency={1} LayoutOrder={0} Size={UDim2.fromOffset(280, 40)}> <Text BackgroundTransparency={1} Font={Enum.Font.GothamBold} Size={UDim2.fromOffset(280, 18)} Text="Email preferences" TextColor3={theme.colors.textPrimary} TextSize={theme.typography.bodyMd.textSize} TextXAlignment={Enum.TextXAlignment.Left} /> <Text BackgroundTransparency={1} Position={UDim2.fromOffset(0, 22)} Size={UDim2.fromOffset(280, 16)} Text="Choose which emails you receive." TextColor3={theme.colors.textSecondary} TextSize={theme.typography.labelSm.textSize} TextXAlignment={Enum.TextXAlignment.Left} /> </frame>
{rows.map((row, index) => ( <Checkbox.Root asChild checked={row.checked} disabled={row.disabled} key={row.label} onCheckedChange={row.onChange} > <textbutton Active={!row.disabled} AutoButtonColor={false} BackgroundTransparency={1} LayoutOrder={index + 1} Selectable={!row.disabled} Size={UDim2.fromOffset(280, 40)} Text="" > <frame BackgroundColor3={row.checked ? theme.colors.accent : theme.colors.surfaceElevated} BackgroundTransparency={row.disabled ? 0.5 : 0} BorderSizePixel={0} Position={UDim2.fromOffset(0, 3)} Size={UDim2.fromOffset(20, 20)} > <uicorner CornerRadius={new UDim(0, theme.radius.sm)} /> <uistroke Color={row.checked ? theme.colors.accent : theme.colors.border} Thickness={1} Transparency={row.disabled ? 0.5 : 0} /> <Checkbox.Indicator asChild> <Text BackgroundTransparency={1} Size={UDim2.fromScale(1, 1)} Text="✓" TextColor3={theme.colors.accentContrast} TextSize={theme.typography.labelSm.textSize} /> </Checkbox.Indicator> </frame> <Text BackgroundTransparency={1} Font={Enum.Font.GothamMedium} Position={UDim2.fromOffset(32, 2)} Size={UDim2.fromOffset(248, 18)} Text={row.label} TextColor3={row.disabled ? theme.colors.textSecondary : theme.colors.textPrimary} TextSize={theme.typography.labelSm.textSize} TextXAlignment={Enum.TextXAlignment.Left} /> <Text BackgroundTransparency={1} Position={UDim2.fromOffset(32, 22)} Size={UDim2.fromOffset(248, 16)} Text={row.description} TextColor3={theme.colors.textSecondary} TextSize={theme.typography.labelSm.textSize} TextXAlignment={Enum.TextXAlignment.Left} /> </textbutton> </Checkbox.Root> ))}
<frame BackgroundTransparency={1} LayoutOrder={4} Size={UDim2.fromOffset(280, 40)}> <textbutton {...(mergeGuiProps(buttonRecipe({ intent: "primary", size: "sm" }, theme), { AnchorPoint: new Vector2(1, 1), Position: UDim2.fromScale(1, 1), Size: UDim2.fromOffset(140, 36), Text: "Save preferences", TextSize: theme.typography.labelSm.textSize, }) as Record<string, unknown>)} > <uicorner CornerRadius={new UDim(0, theme.radius.md)} /> </textbutton> </frame> </frame> );}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.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.
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.
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.
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).
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.
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.
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.
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.
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.
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. |