# Switch

A control that lets the user toggle between checked and not checked.

```tsx
import { Label } from "@/components/ui/label"
import { HStack } from "@/components/ui/stack"
import { Switch } from "@/components/ui/switch"

export default function SwitchDemo() {
  return (
    <HStack gap={2} align="center">
      <Switch id="airplane-mode" name="airplane-mode" />
      <Label htmlFor="airplane-mode">Airplane Mode</Label>
    </HStack>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and paste `states.ts` and
   `state-styles.ts` from the same page. The switch imports both. The examples
   caption their switches with a [Label](https://carbon.style/docs/components/label) or a
   [Field](https://carbon.style/docs/components/field).

   Paste `field-state.ts` from the same installation page.

3. Copy and paste the following code into your project.

   ```tsx title="components/ui/switch.tsx"
   "use client"

   import { use, type CSSProperties } from "react"
   import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"

   import { stateStyles } from "@/lib/state-styles"
   import { FieldStateContext } from "@/lib/field-state"
   import { states } from "@/theme/states"
   import { color, duration, ease, radius, shadow, space } from "@/theme/tokens.stylex"

   const switchTrackBg = `light-dark(${color.input}, color-mix(in oklab, ${color.input} 80%, transparent))`
   const switchThumbBg = `light-dark(${color.background}, ${color.foreground})`
   const switchThumbCheckedBg = `light-dark(${color.background}, ${color.primaryForeground})`

   const styles = stylex.create({
     root: {
       position: "relative",
       display: "inline-flex",
       alignItems: "center",
       flexShrink: 0,
       borderRadius: radius.full,
       borderWidth: 1,
       borderStyle: "solid",
       backgroundColor: switchTrackBg,
       backgroundOrigin: "border-box",
       transitionProperty: "background-color, border-color, box-shadow",
       transitionDuration: duration.color,
       transitionTimingFunction: ease.standard,
       "::after": {
         content: '""',
         position: "absolute",
         insetInline: `calc(-1 * ${space.s3})`,
         insetBlock: `calc(-1 * ${space.s2})`,
       },
     },
     checked: {
       backgroundColor: color.primary,
       backgroundImage: `linear-gradient(to bottom, ${color.buttonPrimarySheen}, transparent)`,
     },
     thumb: {
       position: "relative",
       insetInlineStart: 0,
       width: "var(--carbon-switch-thumb)",
       height: "var(--carbon-switch-thumb)",
       borderRadius: radius.full,
       backgroundColor: switchThumbBg,
       backgroundImage: `linear-gradient(to bottom, transparent, ${color.thumbShade})`,
       boxShadow: shadow.thumb,
       pointerEvents: "none",
       transitionProperty: "inset-inline-start, background-color",
       transitionDuration: {
         default: duration.move,
         "@media (prefers-reduced-motion: reduce)": "0s",
       },
       transitionTimingFunction: ease.spring,
     },
     thumbChecked: {
       insetInlineStart: "calc(100% - var(--carbon-switch-thumb))",
       backgroundColor: switchThumbCheckedBg,
     },
   })

   const sizes = stylex.create({
     default: {
       width: space.s8,
       height: "1.125rem",
       "--carbon-switch-thumb": space.s4,
     },
     sm: {
       width: space.s6,
       height: space.s3_5,
       "--carbon-switch-thumb": space.s3,
     },
   })

   const switchStyles = {
     root: styles.root,
     sizes,
     checked: styles.checked,
     disabled: states.disabled,
     invalid: states.invalidRing,
     thumb: styles.thumb,
     thumbChecked: styles.thumbChecked,
   }

   type SwitchSize = keyof typeof sizes

   type SwitchProps = Omit<SwitchPrimitive.Root.Props, "className" | "style"> & {
     size?: SwitchSize
     xstyle?: StyleXStyles
     style?: CSSProperties
     className?: string
   }

   function Switch({ size = "default", xstyle, style, className, ...props }: SwitchProps) {
     const fieldState = use(FieldStateContext)
     return (
       <SwitchPrimitive.Root
         data-slot="switch"
         data-size={size}
         {...props}
         {...stateStyles(
           (state) => [
             styles.root,
             states.focusRing,
             sizes[size],
             (state.valid === false ||
               fieldState?.valid === false ||
               props["aria-invalid"] === true ||
               props["aria-invalid"] === "true") &&
               states.invalidRing,
             state.checked && styles.checked,
             state.disabled && states.disabled,
             xstyle,
           ],
           { className, style },
         )}
       >
         <SwitchPrimitive.Thumb
           data-slot="switch-thumb"
           {...stateStyles((state) => [styles.thumb, state.checked && styles.thumbChecked])}
         />
       </SwitchPrimitive.Root>
     )
   }

   export { Switch, switchStyles, type SwitchSize }
   ```

4. Update the import paths to match your project setup.

## Usage

```tsx
import { Label } from "@/components/ui/label"
import { HStack } from "@/components/ui/stack"
import { Switch } from "@/components/ui/switch"
```

```tsx
<HStack gap={2} align="center">
  <Switch id="airplane-mode" />
  <Label htmlFor="airplane-mode">Airplane Mode</Label>
</HStack>
```

Match the label's `htmlFor` to the switch's `id`, or wrap the switch in the
label. A switch takes effect the moment it is flipped, so it needs no submit
button beside it. Use a [Checkbox](https://carbon.style/docs/components/checkbox) for a value that
is saved with a form.

The thumb travels on `inset-inline-start`, so it moves toward the inline end in
a right-to-left document.

## Checked state

Use `defaultChecked` for an uncontrolled switch, or `checked` and
`onCheckedChange` to control it.

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

return <Switch checked={enabled} onCheckedChange={setEnabled} />
```

Give the switch a `name` to submit it with a form. Base UI renders a hidden
`<input type="checkbox">` beside the control, and that input carries the value.

## Description

Put the switch in a horizontal `Field` and a `FieldContent` beside it to add a
line of helper text under the label.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Field, FieldContent, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"

const styles = stylex.create({
  field: { maxWidth: "24rem" },
})

export default function SwitchDescription() {
  return (
    <Field orientation="horizontal" xstyle={styles.field}>
      <FieldContent>
        <FieldLabel>Share across devices</FieldLabel>
        <FieldDescription>
          Focus is shared across devices, and turns off when you leave the app.
        </FieldDescription>
      </FieldContent>
      <Switch id="switch-focus-mode" />
    </Field>
  )
}
```

## Choice card

Wrap a whole `Field` in a `FieldLabel` to turn the row into a card, and pass
`checked` to style the cards that are on.

```tsx
"use client"

import { useState } from "react"
import * as stylex from "@stylexjs/stylex"
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldGroup,
  FieldLabel,
  FieldTitle,
} from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"

const styles = stylex.create({
  group: { maxWidth: "24rem" },
})

const settings = [
  {
    id: "share",
    title: "Share across devices",
    description: "Focus is shared across devices, and turns off when you leave the app.",
  },
  {
    id: "notifications",
    title: "Enable notifications",
    description: "Receive notifications when focus mode is enabled or disabled.",
  },
]

export default function SwitchChoiceCard() {
  const [enabled, setEnabled] = useState<Record<string, boolean>>({ notifications: true })

  return (
    <FieldGroup gap={3} xstyle={styles.group}>
      {settings.map((setting) => (
        <FieldLabel
          key={setting.id}
          htmlFor={`setting-${setting.id}`}
          checked={enabled[setting.id] ?? false}
        >
          <Field orientation="horizontal">
            <FieldContent>
              <FieldTitle>{setting.title}</FieldTitle>
              <FieldDescription>{setting.description}</FieldDescription>
            </FieldContent>
            <Switch
              id={`setting-${setting.id}`}
              checked={enabled[setting.id] ?? false}
              onCheckedChange={(checked) =>
                setEnabled((current) => ({ ...current, [setting.id]: checked }))
              }
            />
          </Field>
        </FieldLabel>
      ))}
    </FieldGroup>
  )
}
```

> **Note**
>
> `FieldLabel` does not read the switch's state, so `checked` is a prop you set from
> the same value you give the switch. That makes the card a controlled switch.

## Disabled

Set `disabled` on `Field` to disable its control and dim its label, or on a bare control directly.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"

const styles = stylex.create({
  group: { width: "fit-content" },
  label: { fontWeight: 400 },
})

export default function SwitchDisabled() {
  return (
    <FieldGroup gap={3} xstyle={styles.group}>
      <Field orientation="horizontal" disabled>
        <Switch id="switch-disabled-off" />
        <FieldLabel xstyle={styles.label}>Off</FieldLabel>
      </Field>
      <Field orientation="horizontal" disabled>
        <Switch id="switch-disabled-on" defaultChecked />
        <FieldLabel xstyle={styles.label}>On</FieldLabel>
      </Field>
    </FieldGroup>
  )
}
```

## Invalid

Set `invalid` on `Field` to style its control, or `aria-invalid` on a bare control. The examples show both.

```tsx
import { Switch } from "@/components/ui/switch"
import { Field, FieldLabel, FieldError } from "@/components/ui/field"
import { Label } from "@/components/ui/label"
import { VStack } from "@/components/ui/stack"

export default function SwitchInvalid() {
  return (
    <VStack gap={5} width="100%" maxWidth="24rem">
      <VStack gap={2}>
        <Label htmlFor="switch-invalid-bare">Accept terms and conditions</Label>
        <Switch
          id="switch-invalid-bare"
          aria-invalid
          aria-describedby="switch-invalid-bare-error"
        />
        <FieldError id="switch-invalid-bare-error">
          You must accept the terms and conditions to continue.
        </FieldError>
      </VStack>
      <Field invalid>
        <FieldLabel>Accept terms and conditions</FieldLabel>
        <Switch />
        <FieldError>You must accept the terms and conditions to continue.</FieldError>
      </Field>
    </VStack>
  )
}
```

## Size

Use the `size` prop to change the size of the switch. The thumb and the travel
follow the track, so a `sm` switch needs no other change.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"

const styles = stylex.create({
  group: { width: "fit-content" },
  label: { fontWeight: 400 },
})

export default function SwitchSize() {
  return (
    <FieldGroup gap={3} xstyle={styles.group}>
      <Field orientation="horizontal">
        <Switch id="switch-size-sm" size="sm" defaultChecked />
        <FieldLabel xstyle={styles.label}>Small</FieldLabel>
      </Field>
      <Field orientation="horizontal">
        <Switch id="switch-size-default" size="default" defaultChecked />
        <FieldLabel xstyle={styles.label}>Default</FieldLabel>
      </Field>
    </FieldGroup>
  )
}
```

## API reference

Pass `render` to replace a part's element or compose it with another component.

`Switch` wraps the Base UI `Switch` primitive and accepts its props. See the
[Base UI reference](https://base-ui.com/react/components/switch).

### Switch

| Prop              | Type                         | Default     |
| ----------------- | ---------------------------- | ----------- |
| `size`            | `"sm" \| "default"`          | `"default"` |
| `checked`         | `boolean`                    | —           |
| `defaultChecked`  | `boolean`                    | `false`     |
| `onCheckedChange` | `(checked, details) => void` | —           |
| `name`            | `string`                     | —           |
| `value`           | `string`                     | —           |
| `disabled`        | `boolean`                    | `false`     |
| `readOnly`        | `boolean`                    | `false`     |
| `required`        | `boolean`                    | `false`     |
| `aria-invalid`    | `boolean`                    | —           |
| `id`              | `string`                     | —           |
| `inputRef`        | `Ref<HTMLInputElement>`      | —           |
| `xstyle`          | `StyleXStyles`               | —           |
| `style`           | `CSSProperties`              | —           |
| `className`       | `string`                     | —           |

The root is a `<button role="switch">`. Base UI renders a hidden `<input>`
beside it that carries `id`, `name` and `value`. A label's `htmlFor` and a
form submission reach the control through that input. The component renders the thumb.
There is no `SwitchThumb` part to place.

### switchStyles

The style maps behind the component. Spread them onto any element.

| Key            | What it is                                         |
| -------------- | -------------------------------------------------- |
| `root`         | The track: shape, fill, hit area                   |
| `sizes`        | The `sm` and `default` track sizes, keyed by name  |
| `checked`      | The primary fill and its sheen                     |
| `disabled`     | The dimmed, non-interactive state                  |
| `invalid`      | The destructive border and ring                    |
| `thumb`        | The thumb at rest and the transition it travels on |
| `thumbChecked` | The thumb's far end of the travel and its fill     |

A size sets `--carbon-switch-thumb`, which the thumb reads for its width,
height and the distance it moves. Override that property to resize a switch
without adding a size.

`root` carries no focus ring. The ring, `disabled` and `invalid` all come from
`states` in `theme/states.ts`, and the ring reads a resting border colour from
`--carbon-border-color`. The switch leaves that property unset, so the track's
border stays transparent until focus colours it.

### Data attributes

| Attribute                                         | On           | Effect                  |
| ------------------------------------------------- | ------------ | ----------------------- |
| `data-slot="switch"`                              | the track    | Targets the control     |
| `data-slot="switch-thumb"`                        | the thumb    | Targets the moving part |
| `data-size`                                       | the track    | Mirrors the `size` prop |
| `data-checked`, `data-unchecked`                  | track, thumb | Base UI writes them     |
| `data-disabled`, `data-readonly`, `data-required` | track, thumb | Base UI writes them     |
