Latticecomponents

Switch

Boolean toggle primitive that owns checked state and animates the thumb between the ends of the track, while your component owns every part of how it looks.

@lattice-ui/react-switchStable directionimport Switchdepends on runtime, motion

Switch is the primitive for an on/off toggle: settings flips, feature enables, and any binary control. The root is the toggleable track that owns checked state, and the thumb slides between the two ends as the state changes. Everything visual — the track’s size and color, the thumb’s shape — is yours.

Reach for Switch when a control is boolean, should toggle on activation, and wants a thumb that animates between off and on positions. If you need a third "indeterminate" state or a reveal-style indicator instead of a sliding handle, Checkbox is usually the better fit.

Preview

The component running live in the browser — the same @rbxts/react tree Roblox renders, fully interactive.

Edit

Import

import { Switch } from "@lattice-ui/react-switch";

Anatomy

Root is the toggleable track and is the only required part. Thumb is the sliding handle; include it whenever you want the moving indicator.

Switch anatomy

Switch anatomy
<Switch.Root>
<Switch.Thumb />
</Switch.Root>
PartRequiredResponsibility
Switch.RootyesThe track button: owns checked state and toggles on activation.
Switch.ThumbnoThe handle that animates between the off and on ends of the track.

Examples

Basic usage

The smallest switch that is actually visible: uncontrolled state seeded with defaultChecked, and just enough styling to see it. The root renders a textbutton and the thumb a frame, both unstyled, so the size and colors below are the minimum — without them the switch works but draws nothing.

BasicSwitch.tsx
import { Switch } from "@lattice-ui/react-switch";
export function BasicSwitch() {
return (
<Switch.Root
BackgroundColor3={Color3.fromRGB(66, 73, 91)}
defaultChecked={true}
onCheckedChange={(checked) => print(`switch is now: ${checked}`)}
Size={UDim2.fromOffset(48, 24)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
<Switch.Thumb
BackgroundColor3={Color3.fromRGB(240, 244, 252)}
Size={UDim2.fromOffset(20, 20)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
</Switch.Thumb>
</Switch.Root>
);
}

Controlled settings toggle

Pass checked and onCheckedChange when something outside the switch needs to read or set the value — persisting a setting, syncing with a server, or resetting from elsewhere. Here a music toggle keeps local state for instant feedback and forwards every change to a save callback.

MusicToggle.tsx
import { useState } from "@rbxts/react";
import { Switch } from "@lattice-ui/react-switch";
export function MusicToggle(props: { onSave: (enabled: boolean) => void }) {
const [enabled, setEnabled] = useState(true);
return (
<frame BackgroundTransparency={1} Size={UDim2.fromOffset(280, 36)}>
<uilistlayout
FillDirection={Enum.FillDirection.Horizontal}
Padding={new UDim(0, 12)}
VerticalAlignment={Enum.VerticalAlignment.Center}
/>
<textlabel
BackgroundTransparency={1}
Size={UDim2.fromOffset(100, 20)}
Text="Music"
TextColor3={Color3.fromRGB(236, 241, 249)}
TextXAlignment={Enum.TextXAlignment.Left}
/>
<Switch.Root
BackgroundColor3={enabled ? Color3.fromRGB(86, 141, 255) : Color3.fromRGB(66, 73, 91)}
checked={enabled}
onCheckedChange={(checked) => {
setEnabled(checked);
props.onSave(checked);
}}
Size={UDim2.fromOffset(48, 24)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
<Switch.Thumb
BackgroundColor3={Color3.fromRGB(240, 244, 252)}
Size={UDim2.fromOffset(20, 20)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
</Switch.Thumb>
</Switch.Root>
</frame>
);
}

Custom track and thumb with asChild

Use asChild on the root when you need a frame track rather than the textbutton the root renders, and on the thumb to slide your own handle. Track color is always yours — branch on the controlled state directly. The thumb still animates: its travel resolves to the track width minus the thumb width, whatever size you make either one.

StyledSwitch.tsx
import { useState } from "@rbxts/react";
import { Switch } from "@lattice-ui/react-switch";
export function StyledSwitch() {
const [enabled, setEnabled] = useState(false);
return (
<Switch.Root checked={enabled} onCheckedChange={setEnabled} asChild>
<frame
BackgroundColor3={enabled ? Color3.fromRGB(52, 168, 108) : Color3.fromRGB(44, 48, 60)}
BorderSizePixel={0}
Size={UDim2.fromOffset(48, 24)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
<uistroke Color={Color3.fromRGB(88, 142, 255)} Thickness={1} />
<Switch.Thumb asChild>
<frame
BackgroundColor3={Color3.fromRGB(240, 244, 252)}
BorderSizePixel={0}
Size={UDim2.fromOffset(20, 20)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
</frame>
</Switch.Thumb>
</frame>
</Switch.Root>
);
}

Disabled state

disabled blocks toggling entirely — activation is ignored and setChecked calls from context are dropped — and removes the switch from gamepad selection. Nothing about the appearance changes on its own, so render the inert look yourself. Here a premium-only option stays visible but inactive.

PremiumToggle.tsx
import { Switch } from "@lattice-ui/react-switch";
export function PremiumToggle(props: { hasPremium: boolean }) {
const disabled = !props.hasPremium;
return (
<Switch.Root
BackgroundColor3={disabled ? Color3.fromRGB(84, 90, 106) : Color3.fromRGB(66, 73, 91)}
defaultChecked={false}
disabled={disabled}
Size={UDim2.fromOffset(48, 24)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
<Switch.Thumb
BackgroundColor3={Color3.fromRGB(240, 244, 252)}
Size={UDim2.fromOffset(20, 20)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
</Switch.Thumb>
</Switch.Root>
);
}

Custom thumb size

The thumb’s motion has no transition prop — the slide is a fixed short settle — but the geometry adapts to your handle on its own. Travel resolves to the track width minus the thumb width, and the thumb stays vertically centered at any height, so you do not have to compute insets or match sizes.

LargeThumbSwitch.tsx
import { useState } from "@rbxts/react";
import { Switch } from "@lattice-ui/react-switch";
export function LargeThumbSwitch() {
const [enabled, setEnabled] = useState(false);
return (
<Switch.Root checked={enabled} onCheckedChange={setEnabled} asChild>
<frame BackgroundColor3={Color3.fromRGB(66, 73, 91)} BorderSizePixel={0} Size={UDim2.fromOffset(64, 32)}>
<uicorner CornerRadius={new UDim(1, 0)} />
<Switch.Thumb asChild>
<frame
BackgroundColor3={Color3.fromRGB(240, 244, 252)}
BorderSizePixel={0}
Size={UDim2.fromOffset(28, 28)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
</frame>
</Switch.Thumb>
</frame>
</Switch.Root>
);
}

How it behaves

Checked state

Switch.Root is controllable on checked/onCheckedChange, with defaultChecked for uncontrolled usage (defaulting to false). The state is a plain boolean — activating the root flips it. When disabled, both the toggle and direct setChecked calls from context are ignored, so the state cannot change until the switch is re-enabled.

Activation and selection

Switch.Root renders an activatable textbutton that toggles on Activated, so click, tap, and gamepad activation all work. It carries no size, color or label of its own — pass those as props. It is Active and Selectable only while enabled, so a disabled switch 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 an element that fires Activated, such as a textbutton or imagebutton.

Track color

The track’s BackgroundColor3 is entirely yours. Derive it from the same checked state you pass in, and animate it yourself if you want it to ease rather than snap.

The thumb

Switch.Thumb animates between the two ends of the track as checked changes. Checked parks the thumb’s trailing edge on the track’s trailing edge; unchecked parks its leading edge on the leading edge. Because AnchorPoint and Position interpolate together, the travel resolves to the track width minus the thumb width for any thumb width — the primitive never needs to know how wide you made it, and a thumb sized through a child element, a size constraint, or a layout works as well as one with a declared Size. The same pairing on the Y axis keeps the thumb centered in the track at any height.

Motion owns AnchorPoint and Position under a layout target contract, so both are dropped from anything you pass rather than being written and clobbered on the next frame. Style the thumb with size, color, corners and children instead.

Under asChild, the primitive wraps your element in a transparent frame that it animates, and pins your element to Position (0, 0) inside that wrapper — so put your styling on the child, but leave its Position alone. The thumb is always mounted regardless of checked state; unlike Checkbox.Indicator it is not presence-driven, so there is no forceMount prop.

Motion

The thumb slide is a fixed short response settle — a 0.08s swift, responsive tween — so toggling feels immediate but smooth rather than snapping. Neither Root nor Thumb exposes a transition prop; you shape the feel through geometry instead. Nothing else on the switch animates: since 0.7.0 the track color is yours, so any color transition is yours to drive too.

API reference

Switch.Root

PropTypeDescription
checkedbooleanControlled checked state. Pair with onCheckedChange.
defaultCheckedbooleanInitial checked state for uncontrolled usage. Defaults to false.
onCheckedChange(checked: boolean) => voidCalled whenever the checked state changes.
disabledbooleanPrevents toggling and removes the switch from gamepad selection. Defaults to false.
asChildbooleanMerge the track behavior onto the single child element instead of the textbutton the part renders. The child must be an activatable button.
childrenReact.ReactNodeThe track contents, typically a Switch.Thumb. Must be a single valid element when asChild is set.
…TextButton propsPartial<WritableInstanceProperties<TextButton>>Forwarded onto the rendered textbutton and type-checked against it. Active and Selectable are owned by the primitive, derived from disabled.

Switch.Thumb

Renders a Frame. Unknown props forward onto it and are type-checked against it, so a prop Frame does not accept is a compile error. The primitive owns AnchorPoint and Position under a layout motion contract, so values you pass for those are ignored.

PropTypeDescription
asChildbooleanMerge the animated thumb onto the single child element instead of the frame the part renders. The child is pinned at Position (0, 0) inside the animated wrapper.
childrenReact.ReactNodeThe thumb contents. Must be a single valid element when asChild is set.
…Frame propsPartial<WritableInstanceProperties<Frame>>Forwarded onto the rendered frame and type-checked against it. AnchorPoint and Position are dropped — motion owns the thumb's placement. Travel is derived from the track and thumb widths, so no Size is required.