Facetcomponents

Switch

A track and a thumb, where the thumb's travel is the primitive's job and this file only says what it looks like.

Terminal window
npx facet-rbxts add switch

Copies ui/switch.tsx, plus lib/utils.ts. Needs @facet-ui/react-variants, @lattice-ui/react-runtime@^0.8.0 and @lattice-ui/react-switch@^0.8.0.

import { Switch } from "../shared/ui/switch";
<Switch defaultChecked onCheckedChange={(checked) => setMuted(!checked)} />
Off, on, and disabled. The thumb's position is Lattice's; the track's colour is this file's.

Renders a TextButton. Unknown props forward onto it and are type-checked against it, so a prop TextButton does not accept is a compile error. The primitive owns AnchorPoint and Position on the thumb, which it animates between the track's edges, so values you pass for those are ignored.

Props

PropTypeDescription
checkedbooleanControlled value. Pass it with onCheckedChange.
defaultCheckedbooleanUncontrolled starting value. Defaults to false.
onCheckedChange(checked: boolean) => voidFires on every change, controlled or not.
disabledbooleanBlocks the press and adds opacity-50 to the recipe's className slot.
classNameClassNameThreaded into the recipe's className slot inside the component. A class written at a Vela-compiled call site never reaches it — see Overriding from the call site.

No indeterminate here, unlike Checkbox — a switch is on or off, and Lattice types it boolean.

Two classes, and one of them is the whole component

export const switchVariants = {
root: fv("h-5 w-9 rounded-full transition duration-150"),
thumb: fv("size-4 rounded-full bg-background"),
};

The root carries no background colour at all. It is applied from the mirrored state instead:

const className = switchVariants.root({
className: cn(checked ? "bg-primary" : "bg-input", disabled && "opacity-50", props.className),
});

That is the same mirrored-state shape checkbox establishes — useControllableState here, the primitive driven controlled from it — because Lattice keeps its context private and the track’s colour changes with the value.

The thumb has no position

This is layout is an instance, not a property seen from the other side: a Lattice primitive that owns a property is as load-bearing as a UIListLayout that owns one, and the recipe stays out of both.

Sizing it

h-5 w-9 on the track and size-4 on the thumb are the only numbers in the component, and they are paired — a 16px thumb inside a 20px track leaves 2px of inset on each edge. Changing one means changing the other; there is no variant that does it for you, because the file is short enough to edit.

A className at the call site cannot do it either. h-5 w-9 is in the recipe, and the recipe resolves after the props a call-site class arrives as — see overriding from the call site.