Latticecomponents

Switch

A boolean toggle that animates its thumb between the ends of the track.

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

Switch is the primitive for an on/off toggle: the root is the toggleable track that owns checked state, and the thumb slides between its two ends. Everything visual is yours. For a third "indeterminate" state or a reveal-style indicator, Checkbox fits better.

Preview

Edit

Import

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

Anatomy

Root is the toggleable track and the only required part. Thumb is the sliding handle.

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

Uncontrolled state seeded with defaultChecked, plus enough styling to see it. The root renders a textbutton and the thumb a frame, both unstyled, so without a size and colours 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 needs to read or set the value. 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 for a frame track rather than a textbutton, and on the thumb to slide your own handle. Track colour is always yours. The thumb still animates: its travel resolves to the track width minus the thumb width, whatever size you make either.

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 and removes the switch from gamepad selection. Nothing about the appearance changes on its own, so render the inert look yourself.

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 slide is a fixed short settle with no transition prop, but the geometry adapts. Travel resolves to the track width minus the thumb width, and the thumb stays vertically centred at any height.

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

Controllable on checked/onCheckedChange, with defaultChecked for uncontrolled usage. The state is a plain boolean that activation flips. While disabled, both the toggle and direct setChecked calls are ignored.

Activation and selection

Switch.Root renders an activatable textbutton that toggles on Activated. It carries no size, colour or label of its own, and is Active and Selectable only while enabled.

Under asChild the slot’s Active, Selectable and ref win over the child’s props, and event handlers compose. Use an element that fires Activated.

Track color

The track’s BackgroundColor3 is entirely yours — derive it from the same checked state you pass in.

The thumb

Switch.Thumb animates between the ends of the track as checked changes. Checked parks its trailing edge on the track’s trailing edge, unchecked its leading edge on the leading edge. AnchorPoint and Position interpolate together, so travel resolves to the track width minus the thumb width for any thumb width. The same pairing on Y keeps it centred at any height.

Motion owns AnchorPoint and Position under a layout target contract, so both are dropped from anything you pass. Style the thumb with size, colour, corners and children.

Under asChild the primitive wraps your element in a transparent frame it animates and pins your element to Position (0, 0) inside it. The thumb is always mounted, so there is no forceMount.

Motion

The slide is a fixed 0.08s settle. Neither Root nor Thumb exposes a transition — you shape the feel through geometry. Nothing else animates.

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.