---
name: switch
type: core
domain: forms
requires: [loke-ui]
description: >
  Switch + SwitchThumb for binary on/off toggles. role=switch semantics, no indeterminate
  state. Hidden native checkbox input for form participation. Label required for accessible
  name. Use Checkbox for tri-state or multi-select scenarios.
---

# Switch

`@loke/ui/switch` — toggle switch built on `Primitive.button` with `role="switch"`. A hidden `<input type="checkbox">` handles form participation. No indeterminate state.

**Exports:** `Switch`, `SwitchThumb`, `createSwitchScope`

## Setup

```tsx
import { Switch, SwitchThumb } from "@loke/ui/switch";
import { Label } from "@loke/ui/label";

function NotificationToggle() {
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
      <Label htmlFor="notifications">Email notifications</Label>
      <Switch id="notifications" name="notifications" defaultChecked>
        <SwitchThumb />
      </Switch>
    </div>
  );
}
```

`data-state` on `Switch` and `SwitchThumb`: `"checked"` | `"unchecked"`

## Core Patterns

### Controlled state

```tsx
const [enabled, setEnabled] = useState(false);

<Switch
  checked={enabled}
  onCheckedChange={setEnabled}
  name="darkMode"
>
  <SwitchThumb />
</Switch>
```

### Form participation

Inside a `<form>`, a hidden `<input type="checkbox">` is rendered automatically. Use `name` and `value` props to control the submitted field.

```tsx
<form onSubmit={handleSubmit}>
  <Switch name="marketing" value="yes" defaultChecked={false}>
    <SwitchThumb />
  </Switch>
  <button type="submit">Save</button>
</form>
```

For a Switch outside its form element, pass the `form` prop with the form's `id`:

```tsx
<Switch name="analytics" form="settings-form" defaultChecked>
  <SwitchThumb />
</Switch>
```

## Common Mistakes

### 1. Using Switch when Checkbox semantics are needed

Switch uses `role="switch"` — a binary on/off control. It has no indeterminate state and no multi-select semantics. Screen readers announce it as "on" or "off".

**Wrong:** Using Switch for selecting items in a list, or for a tri-state header row in a table.

**Correct:** Use `Checkbox` with `checked="indeterminate"` for tri-state. Use `Checkbox` for selecting items from a set. Use `Switch` only for settings-style on/off toggles.

Source: `src/components/switch/switch.tsx` — `role="switch"`, no indeterminate branch

### 2. Forgetting SwitchThumb

`Switch` renders the track button only. `SwitchThumb` is the sliding indicator element that moves between checked and unchecked positions. Without it the switch has no visible affordance.

**Wrong:**

```tsx
<Switch name="active" />
```

**Correct:**

```tsx
<Switch name="active">
  <SwitchThumb />
</Switch>
```

`SwitchThumb` reads checked state from context — it must be a child of `Switch`.

Source: `src/components/switch/switch.tsx` — `SwitchThumb` uses `useSwitchContext`

### 3. Missing label — no accessible name

Switch renders as `<button role="switch">`. Without an associated Label, screen readers announce the control without context.

**Wrong:**

```tsx
<Switch name="wifi"><SwitchThumb /></Switch>
<span>Wi-Fi</span>
```

**Correct:**

```tsx
<Label htmlFor="wifi">Wi-Fi</Label>
<Switch id="wifi" name="wifi"><SwitchThumb /></Switch>
```

Or wrap:

```tsx
<Label>
  Wi-Fi
  <Switch name="wifi"><SwitchThumb /></Switch>
</Label>
```

Source: `src/components/switch/switch.tsx` — renders `Primitive.button`

## Cross-references

- **Label** (`@loke/ui/label`) — accessible labelling for Switch
- **Checkbox** (`@loke/ui/checkbox`) — tri-state and multi-select scenarios
- **Choosing the Right Component** — Switch vs Checkbox decision guide
