# 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.

Source: https://docs.astra-void.xyz/facet/components/switch/

```bash
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`.

```tsx
import { Switch } from "../shared/ui/switch";

<Switch defaultChecked onCheckedChange={(checked) => setMuted(!checked)} />
```

_Interactive preview: 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

| Prop | Type | Description |
| --- | --- | --- |
| `checked` | `boolean` | Controlled value. Pass it with onCheckedChange. |
| `defaultChecked` | `boolean` | Uncontrolled starting value. Defaults to false. |
| `onCheckedChange` | `(checked: boolean) => void` | Fires on every change, controlled or not. |
| `disabled` | `boolean` | Blocks the press and adds opacity-50 to the recipe's className slot. |
| `className` | `ClassName` | Threaded 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](https://docs.astra-void.xyz/facet/components/checkbox.md) — a switch is on or off, and
Lattice types it `boolean`.

## Two classes, and one of them is the whole component

```tsx
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:

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

That is the same [mirrored-state](https://docs.astra-void.xyz/facet/components/checkbox.md#the-state-is-mirrored-not-reached-for)
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

> **Nothing in this file says where the thumb goes**
>
> `Switch.Thumb` owns `AnchorPoint` and `Position`, and animates them between the track's edges for
> whatever size the thumb turns out to be. The recipe says `size-4 rounded-full bg-background` and
> stops.
>
> Write a `left-*` or a `translate-x-*` here and you are fighting the primitive on a property it
> rewrites every frame. If you want a different travel, change the thumb's *size* or the track's — the
> geometry follows from those.

This is [layout is an instance, not a property](https://docs.astra-void.xyz/facet/guides/component-conventions.md#5-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](https://docs.astra-void.xyz/facet/guides/variants-and-classes.md#overriding-from-the-call-site).
