# Switch

> Turn a setting on or off and show its current state.

Source: https://www.honestui.com/docs/components/switch

```tsx
import { Label } from "@/components/honest-ui/ui/label"
import { Switch } from "@/components/honest-ui/ui/switch"

export function SwitchWithLabel() {
  return (
    <Label>
      <Switch />
      Marketing emails
    </Label>
  )
}

```

## Overview [#overview]

Use Switch for a single on/off setting whose state is worth showing at a glance: feature flags, notification preferences, privacy modes. The control communicates current state first and invites change second, which makes it the right choice when the setting is already configured and people just adjust it.

Use Checkbox instead when the option is one of several independent choices gathered for a form submission, and use Button when the action runs once rather than flipping a persistent state. If you cannot say what the switch's "on" means in a short label, the setting probably needs its own page, not a toggle.

## Anatomy [#anatomy]

A switch has a track, a thumb, a label, an optional description, and a checked state. The track is 30 × 18 px; the thumb slides to the right end when checked and stretches slightly while pressed so the interaction feels physical even before the state changes.

The label names the setting, not the action: `Marketing emails` works because it reads as a thing that can be on or off. Labels like `Enable` or `On` fail because they describe the click, and the sentence "On: enabled" tells nobody anything.

## Behavior [#behavior]

**Immediate effect.** A switch should apply its change as soon as it flips — that is the mental model people bring to toggles. If the change only takes effect after pressing Save, the switch shows a state that is not real; use a checkbox in a form for that case.

**Pending changes.** When the flip triggers server work, keep the requested position visible, block further clicks while the request runs, and report the outcome nearby. See [async save](#async-save) for the pattern. If the request fails, return the switch to its previous position and say so in text.

**Form submission.** Like a native checkbox, the switch submits its `name`/`value` only while checked. Set `uncheckedValue` when the server needs to distinguish "absent from the form" from "explicitly off".

**Disabled and read-only.** `disabled` dims the switch, blocks pointer input, and removes it from keyboard focus. `readOnly` keeps the switch focusable and submittable but ignores clicks — useful when the value is managed elsewhere but must stay visible.

## Accessibility [#accessibility]

The switch renders a span with `role="switch"` plus a visually hidden native input, wrapped in a `Label`. <kbd>Space</kbd> toggles it; <kbd>Enter</kbd> also activates it through Base UI's button-style key handling. Focus moves with <kbd>Tab</kbd>, and the ring appears only for keyboard focus (`focus-visible`) with an offset against the page background.

The visible label association matters more than usual here because the control itself has no text: screen readers announce "switch, on" or "switch, off" followed by the label, so an unlabeled switch announces nothing usable. Wrapping in a `Label` also makes the words clickable, which compensates for the small target.

The rendered track is 18 px tall — far under the 44 px comfortable touch target, and this component adds no coarse-pointer expansion layer. Rely on the wrapping label for tap area, or add padding around the whole label row in settings lists.

Colors come from theme tokens: the checked track uses the primary token and the unchecked track uses the input token, both of which adapt to dark mode along with the thumb and inset shadow. The thumb position uses a fixed physical translation rather than logical properties, so it travels the same visual direction in right-to-left locales; the label row around it mirrors normally through flexbox.

There is no error or invalid styling on this component; validation feedback belongs in surrounding text via Field if a switch participates in a validated form.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;switch&#x22;]" />
  

  
    
      
        Install the following dependencies:
      

      ```bash
      npm install @base-ui-components/react
      ```

      
        Copy and paste the following code into your project.
      

      ### components/ui/switch.tsx

```tsx
"use client"

import { Switch as SwitchPrimitive } from "@base-ui-components/react/switch"

import { cn } from "@/lib/utils"

function Switch({ className, ...props }: SwitchPrimitive.Root.Props) {
  return (
    <SwitchPrimitive.Root
      data-slot="switch"
      className={cn(
        "group/switch inline-flex h-[1.125rem] w-7.5 shrink-0 items-center rounded-full p-px inset-shadow-[0_1px_--theme(--color-black/4%)] transition-all outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-64 data-checked:bg-primary data-unchecked:bg-input",
        className
      )}
      {...props}
    >
      <SwitchPrimitive.Thumb
        data-slot="switch-thumb"
        className={cn(
          "pointer-events-none block size-4 rounded-full bg-background shadow-sm transition-[translate,width] group-active/switch:w-4.5 data-checked:translate-x-3 data-checked:group-active/switch:translate-x-2.5 data-unchecked:translate-x-0"
        )}
      />
    </SwitchPrimitive.Root>
  )
}

export { Switch }

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import { Switch } from "@/components/ui/switch";
```

```tsx
<Label>
  <Switch defaultChecked />
  Marketing emails
</Label>
```

A bare `<Switch />` has no accessible name. Wrap it in a `Label`, or pass `aria-label` when no visible label fits. Use controlled state (`checked` + `onCheckedChange`) only when the application owns the value.

## Don't do this [#dont-do-this]

### Labeling the action instead of the setting [#labeling-the-action-instead-of-the-setting]

```tsx
// Bad
<Label>
  <Switch />
  Enable notifications
</Label>
```

```tsx
// Good
<Label>
  <Switch />
  Notifications
</Label>
```

A switch displays whether something is on, so `Enable notifications` reads backwards when off: is enabling disabled? Settings written as nouns survive screen-reader announcements ("Notifications, switch, off") and translation better than verb phrases, which assume a state the reader cannot see yet.

### Assuming an unchecked switch submits a value [#assuming-an-unchecked-switch-submits-a-value]

```tsx
// Bad
const enabled = formData.get("marketing") === "on";
// An unchecked switch never sends "marketing", so this branch never sees false.

// Good
<Switch name="marketing" uncheckedValue="off" />
const enabled = formData.get("marketing") !== "off";
```

Like a native checkbox, an unchecked switch contributes nothing to the form data, so absence must mean "off". That inference breaks the moment another field could be missing or a proxy drops values. `uncheckedValue` makes both states explicit in the submission.

### Flipping silently while the work happens [#flipping-silently-while-the-work-happens]

```tsx
// Bad
<Switch
  checked={checked}
  onCheckedChange={async (next) => {
    await saveSetting(next);
    setChecked(next);
  }}
/>

// Good
<Switch
  checked={checked}
  disabled={pending}
  onCheckedChange={(next) => void update(next)}
/>
<p role="status">{pending ? "Saving…" : error ? "Couldn't save. Try again." : ""}</p>
```

The bad version leaves the switch interactive during the request, so double-clicks fire two saves, and if the request fails there is no signal that reality diverges from the picture. Disable while pending, revert on failure, and narrate the outcome in text next to the control — the full pattern is in [Async save](#async-save).

## Examples [#examples]

Examples cover disabled, described, card-style, async saving, and form-connected switches.

For accessible labeling and validation, use `Field` to connect the switch with its label, description, and error. See the [Field examples](/docs/components/field#examples).

### Disabled [#disabled]

```tsx
import { Label } from "@/components/honest-ui/ui/label"
import { Switch } from "@/components/honest-ui/ui/switch"

export function SwitchWithLabel() {
  return (
    <Label>
      <Switch disabled />
      Marketing emails
    </Label>
  )
}

```

### With Description [#with-description]

```tsx
import * as React from "react"

import { Label } from "@/components/honest-ui/ui/label"
import { Switch } from "@/components/honest-ui/ui/switch"

export function SwitchWithDescriptionDemo() {
  const id = React.useId()

  return (
    <div className="flex items-start gap-2">
      <Switch id={id} defaultChecked />
      <div className="flex flex-col gap-1">
        <Label htmlFor={id}>Marketing emails</Label>
        <p className="text-xs text-muted-foreground">
          By enabling marketing emails, you agree to receive emails.
        </p>
      </div>
    </div>
  )
}

```

### Card Style [#card-style]

```tsx
import * as React from "react"

import { Label } from "@/components/honest-ui/ui/label"
import { Switch } from "@/components/honest-ui/ui/switch"

export function SwitchCardDemo() {
  const id = React.useId()

  return (
    <Label
      htmlFor={id}
      className="flex items-center gap-6 rounded-lg border p-3 hover:bg-accent/50 has-data-checked:border-primary/48 has-data-checked:bg-accent/50"
    >
      <div className="flex flex-col gap-1">
        <p className="text-sm leading-4">Enable notifications</p>
        <p className="text-xs text-muted-foreground">
          You can enable or disable notifications at any time.
        </p>
      </div>
      <Switch id={id} defaultChecked />
    </Label>
  )
}

```

### Async Save [#async-save]

While the preference saves, the switch disables and a spinner plus status text report progress. Turning it off simulates a failed request: the switch returns to its previous position and the failure is announced.

```tsx
"use client"

import * as React from "react"

import { LoaderCircle as LoaderCircleIcon } from "honestui/icons"
import { Label } from "@/components/honest-ui/ui/label"
import { Switch } from "@/components/honest-ui/ui/switch"

async function savePreference(enabled: boolean): Promise<void> {
  await new Promise((resolve) => setTimeout(resolve, 800))
  if (!enabled) {
    throw new Error("Could not reach the server")
  }
}

export function SwitchPendingDemo() {
  const [checked, setChecked] = React.useState(true)
  const [pending, setPending] = React.useState(false)
  const [failed, setFailed] = React.useState(false)

  const onCheckedChange = async (next: boolean) => {
    setPending(true)
    setFailed(false)
    try {
      await savePreference(next)
      setChecked(next)
    } catch {
      setFailed(true)
    } finally {
      setPending(false)
    }
  }

  return (
    <div className="grid w-auto gap-1">
      <Label className="gap-2">
        <Switch
          checked={checked}
          disabled={pending}
          onCheckedChange={onCheckedChange}
        />
        {pending && (
          <LoaderCircleIcon
            className="size-4 animate-spin text-muted-foreground"
            aria-hidden="true"
          />
        )}
        Marketing emails
      </Label>
      <p className="text-sm text-muted-foreground" role="status">
        {pending
          ? "Saving…"
          : failed
            ? "Couldn't save. Try again."
            : checked
              ? "On"
              : "Off"}
      </p>
    </div>
  )
}

```

### Form Integration [#form-integration]

```tsx
"use client";

import * as React from "react";

import { Button } from "@/components/honest-ui/ui/button";
import { Field, FieldLabel } from "@/components/honest-ui/ui/field";
import { Form } from "@/components/honest-ui/ui/form";
import { Switch } from "@/components/honest-ui/ui/switch";

export function SwitchFormDemo() {
  const [status, setStatus] = React.useState("");

  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const enabled = formData.has("marketing");
    setStatus(
      `Submitted preference: marketing emails ${enabled ? "enabled" : "disabled"}.`,
    );
  };

  return (
    <Form onSubmit={onSubmit} className="grid w-auto gap-4">
      <Field name="marketing">
        <FieldLabel>
          <Switch name="marketing" defaultChecked />
          Enable marketing emails
        </FieldLabel>
      </Field>
      <Button type="submit">Save preference</Button>
      <p className="text-sm text-muted-foreground" role="status">
        {status}
      </p>
    </Form>
  );
}

```

## API reference [#api-reference]

`Switch` forwards Base UI Switch root props:

| Prop                                             | Type              | Default      |
| ------------------------------------------------ | ----------------- | ------------ |
| `checked` / `defaultChecked` + `onCheckedChange` | boolean, callback | uncontrolled |
| `name` / `value` / `uncheckedValue`              | string            | —            |
| `disabled`                                       | boolean           | `false`      |
| `readOnly`                                       | boolean           | `false`      |
| `required`                                       | boolean           | `false`      |

Controlled state uses `checked` with `onCheckedChange`; uncontrolled uses `defaultChecked`. The hidden native input submits `name` and `value` only while checked unless `uncheckedValue` provides the off-state value. `inputRef` reaches the hidden input when a form library needs direct access.

See the [Base UI Switch API](https://base-ui.com/react/components/switch#api-reference).
