@lattice-ui/react-checkboxStable directionimport Checkboxdepends on runtime, layer, motionCheckbox 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.
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, so nothing is drawn until you give it a size and colours.
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.
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.
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.
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.
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.
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.
Toggling never yields "indeterminate" — it maps indeterminate to true and otherwise flips the boolean. Provide it through the controlled checked prop, usually derived from a group of child checkboxes.
The root writes no colour 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". For a different glyph, branch on the checked value inside the 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. |