# Switch

Turn a setting on or off.

Use a switch for a setting that takes effect as soon as it changes, like Airplane mode or a notification preference. Keep its label the same in both states: the label says what the setting is, and the switch shows whether it's on.

```vue title="basic.vue"
<script setup lang="ts">
import { Switch, SwitchThumb } from "@ui-sdk/vue/switch"
</script>

<template>
  <label data-slot="label">
    Airplane mode
    <Switch>
      <SwitchThumb />
    </Switch>
  </label>
</template>
```

Click Airplane mode, or focus the switch and press Space, to turn it on and off.

## Installation

```bash
npm install @ui-sdk/vue @ui-sdk/core
```

## Composition

Use the following composition to build a `Switch`:

```text
Switch
└── SwitchThumb
```

Wrap the switch and its text in a label. The thumb stays mounted and gets the switch's state, so CSS can slide it from one end to the other.

## Usage

Import the parts and put them together:

```vue
<script setup lang="ts">
import { Switch, SwitchThumb } from "@ui-sdk/vue/switch"
</script>

<template>
  <label>
    Airplane mode
    <Switch>
      <SwitchThumb />
    </Switch>
  </label>
</template>
```

## Description

Use `FieldLabel` and `FieldDescription` when a setting needs more explanation. The field connects both to the switch.

```vue title="description.vue"
<script setup lang="ts">
import { Field, FieldDescription, FieldLabel } from "@ui-sdk/vue/field"
import { Switch, SwitchThumb } from "@ui-sdk/vue/switch"
</script>

<template>
  <Field>
    <div data-slot="stack">
      <FieldLabel>Weekly digest</FieldLabel>
      <FieldDescription>A summary of your week, every Monday.</FieldDescription>
    </div>
    <Switch>
      <SwitchThumb />
    </Switch>
  </Field>
</template>
```

Click Weekly digest. The field connects the label and description to the switch.

## Choice Card

A setting can fill a whole card. Here the label's `::after` covers the card, so a click anywhere on it toggles the switch, and `:has([data-checked])` marks the cards that are on.

```vue title="choice-card.vue"
<script setup lang="ts">
import { Field, FieldDescription, FieldLabel } from "@ui-sdk/vue/field"
import { Switch, SwitchThumb } from "@ui-sdk/vue/switch"

const settings = [
  {
    label: "Public profile",
    description: "Anyone can see your name and photo.",
    defaultChecked: false,
  },
  {
    label: "Show activity status",
    description: "Teammates can see when you're online.",
    defaultChecked: true,
  },
]
</script>

<template>
  <div data-slot="stack">
    <Field v-for="setting in settings" :key="setting.label" data-variant="card">
      <div data-slot="stack">
        <FieldLabel>{{ setting.label }}</FieldLabel>
        <FieldDescription>{{ setting.description }}</FieldDescription>
      </div>
      <Switch :default-checked="setting.defaultChecked">
        <SwitchThumb />
      </Switch>
    </Field>
  </div>
</template>
```

Click anywhere on the Public profile card to turn it on. Each card's switch keeps its own value.

## Disabled

`disabled` prevents focus and changes, and leaves the switch out of form submissions. A disabled `fieldset` does the same.

```vue title="disabled.vue"
<script setup lang="ts">
import { Switch, SwitchThumb } from "@ui-sdk/vue/switch"
</script>

<template>
  <div data-slot="stack">
    <label data-slot="label">
      Product updates
      <Switch>
        <SwitchThumb />
      </Switch>
    </label>
    <label data-slot="label">
      Security alerts
      <Switch default-checked disabled>
        <SwitchThumb />
      </Switch>
    </label>
  </div>
</template>
```

Security alerts are always on, so that switch is disabled. Clicking it leaves it on, and Tab skips it.

## Read-Only

`read-only` keeps the switch focusable and included in form submissions, while preventing the user from changing it. Use it for a setting managed somewhere else.

```vue title="read-only.vue"
<script setup lang="ts">
import { Field, FieldDescription, FieldLabel } from "@ui-sdk/vue/field"
import { Switch, SwitchThumb } from "@ui-sdk/vue/switch"
</script>

<template>
  <Field>
    <div data-slot="stack">
      <FieldLabel>Encrypt backups</FieldLabel>
      <FieldDescription
        >Your organization manages this setting.</FieldDescription
      >
    </div>
    <Switch name="encrypt" default-checked read-only>
      <SwitchThumb />
    </Switch>
  </Field>
</template>
```

Tab to Encrypt backups. Space and clicks leave it on.

## Controlled

Bind `v-model:checked`, or pass `checked` and handle `@checked-change`, to own the value. A [controller](#controller) from `createSwitchController()` changes it from anywhere, like the Toggle button below. See [State and Events](/docs/vue/state-and-events) for the shared change callback.

```vue title="controlled.vue"
<script setup lang="ts">
import {
  createSwitchController,
  type SwitchCheckedChangeReason,
} from "@ui-sdk/core/switch"
import { Switch, SwitchThumb } from "@ui-sdk/vue/switch"
import { ref } from "vue"
import { useLog } from "./log"

const log = useLog()
const wifi = createSwitchController()
const checked = ref(true)

function onCheckedChange(
  next: boolean,
  details: { reason: SwitchCheckedChangeReason }
) {
  log(`${next ? "On" : "Off"} (${details.reason})`)
}
</script>

<template>
  <div data-slot="stack">
    <div data-slot="controls">
      <button type="button" @click="wifi.toggle()">Toggle</button>
    </div>
    <label data-slot="label">
      Wi-Fi
      <Switch
        v-model:checked="checked"
        :controller="wifi"
        @checked-change="onCheckedChange"
      >
        <SwitchThumb />
      </Switch>
    </label>
  </div>
</template>
```

Turn Wi-Fi off, or press Toggle. The console logs each change and its reason.

## Form

Set `name` to include the switch in a native form. A switch that's on submits its `value`, which defaults to `"on"`; a switch that's off submits nothing. Reset returns it to `default-checked`. Set `form` to a form's id to join it from elsewhere on the page.

```vue title="form.vue"
<script setup lang="ts">
import { FieldLegend, FieldSet } from "@ui-sdk/vue/field"
import { Switch, SwitchThumb } from "@ui-sdk/vue/switch"
import { useLog } from "./log"

const log = useLog()

function onSubmit(event: Event) {
  const entries = [...new FormData(event.currentTarget as HTMLFormElement)].map(
    ([name, value]) => `${name}=${value}`
  )
  log(entries.length ? `Submitted ${entries.join("&")}` : "Submitted nothing")
}
</script>

<template>
  <form data-slot="stack" @submit.prevent="onSubmit">
    <FieldSet>
      <FieldLegend>Email notifications</FieldLegend>
      <label data-slot="label">
        Product updates
        <Switch name="updates">
          <SwitchThumb />
        </Switch>
      </label>
      <label data-slot="label">
        Security alerts
        <Switch name="security" default-checked>
          <SwitchThumb />
        </Switch>
      </label>
      <label data-slot="label">
        Weekly digest
        <Switch name="digest" value="weekly" default-checked>
          <SwitchThumb />
        </Switch>
      </label>
    </FieldSet>
    <div data-slot="row">
      <button type="submit" data-variant="solid">Save</button>
      <button type="reset">Reset</button>
    </div>
  </form>
</template>
```

Turn on Product updates and press Save. The console logs the submitted values, and Reset restores the defaults.

## Validation

`required` blocks submission until the switch is on. In a [Field](/docs/vue/components/field), `FieldError` shows the message under the switch and connects it to the switch. [Forms and Validation](/docs/vue/forms) covers the shared field behavior.

```vue title="invalid.vue"
<script setup lang="ts">
import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from "@ui-sdk/vue/field"
import { Switch, SwitchThumb } from "@ui-sdk/vue/switch"
import { useLog } from "./log"

const log = useLog()

function onSubmit(event: Event) {
  log(
    `Submitted two-factor=${new FormData(event.currentTarget as HTMLFormElement).get("two-factor")}`
  )
}
</script>

<template>
  <form data-slot="stack" @submit.prevent="onSubmit">
    <Field>
      <div data-slot="row">
        <div data-slot="stack">
          <FieldLabel>Two-factor authentication</FieldLabel>
          <FieldDescription
            >Required for admins of this workspace.</FieldDescription
          >
        </div>
        <Switch name="two-factor" required>
          <SwitchThumb />
        </Switch>
      </div>
      <FieldError match="valueMissing">Turn this on to continue.</FieldError>
    </Field>
    <div data-slot="row">
      <button type="submit" data-variant="solid">Continue</button>
    </div>
  </form>
</template>
```

Press Continue with the switch off to see the error. Turn on Two-factor authentication and continue; the console logs the submitted value.

## Saving on Toggle

To save a change before it shows, call `details.cancel()` in `@checked-change` to keep the current value while the save runs. When it finishes, call `controller.setChecked(next, { reason: "imperative" })`, and let that reason through in the handler so the saved value commits.

The example ignores presses while a save is running, and shows a spinner in the thumb. `aria-busy` tells assistive technology the switch is busy; the handler is what blocks another change.

```vue title="async.vue"
<script setup lang="ts">
import { LoaderCircleIcon } from "@lucide/vue"
import {
  createSwitchController,
  type SwitchCheckedChangeReason,
} from "@ui-sdk/core/switch"
import { Switch, SwitchThumb } from "@ui-sdk/vue/switch"
import { ref } from "vue"
import { useLog } from "./log"

const save = (_checked: boolean) =>
  new Promise((resolve) => setTimeout(resolve, 800))

const log = useLog()
const backups = createSwitchController()
const saving = ref(false)

function onCheckedChange(
  next: boolean,
  details: { reason: SwitchCheckedChangeReason; cancel: () => void }
) {
  if (details.reason === "imperative") {
    return
  }
  details.cancel()
  if (saving.value) {
    return
  }
  saving.value = true
  log("Saving…")
  save(next).then(() => {
    backups.setChecked(next, { reason: "imperative" })
    saving.value = false
    log(`Saved: ${next ? "on" : "off"}`)
  })
}
</script>

<template>
  <label data-slot="label">
    Automatic backups
    <Switch
      :controller="backups"
      :aria-busy="saving"
      @checked-change="onCheckedChange"
    >
      <SwitchThumb>
        <LoaderCircleIcon
          v-if="saving"
          data-slot="spinner"
          aria-hidden="true"
        />
      </SwitchThumb>
    </Switch>
  </label>
</template>
```

Turn on Automatic backups. The thumb shows a spinner while it saves, then the switch turns on.

## Custom Rendering

The switch and thumb take a `render` slot, which receives the props to bind and the part's state. Keep the part's props on the element so its role, label and keyboard behavior stay connected. See [Parts and Composition](/docs/vue/parts-and-composition) for the rendering contract.

```vue title="render.vue"
<script setup lang="ts">
import { MoonIcon, SunIcon } from "@lucide/vue"
import { Switch, SwitchThumb } from "@ui-sdk/vue/switch"
</script>

<template>
  <label data-slot="label">
    Dark mode
    <Switch>
      <SwitchThumb>
        <template #render="{ props, state }">
          <span v-bind="props">
            <MoonIcon v-if="state.checked" aria-hidden="true" />
            <SunIcon v-else aria-hidden="true" />
          </span>
        </template>
      </SwitchThumb>
    </Switch>
  </label>
</template>
```

Turn on Dark mode. The thumb renders a sun or a moon from its state.

## Styling

Style the switch and its thumb with `data-checked` and `data-unchecked`, and use `data-disabled`, `data-readonly` and `data-required` for the rest. In an invalid field, the switch also gets `data-invalid` and `aria-invalid`; the thumb doesn't. Slide the thumb with `translate` on `data-checked`, and reverse it under `dir="rtl"`.

Each part has a `data-slot`, such as `switch`, and its state attributes are listed under [Data Attributes](#data-attributes).

## Data Attributes

### Switch
`[data-slot="switch"]`

- `data-checked`: Present while on.
- `data-disabled`: Present when disabled, by its own prop or a `<fieldset>`.
- `data-readonly`: Present when read-only.
- `data-required`: Present when required.
- `data-unchecked`: Present while off.
- `data-invalid`: Present while a `Field` around it shows as invalid. The thumb doesn't get it.

### SwitchThumb
`[data-slot="switch-thumb"]`

- `data-checked`: Present while on.
- `data-disabled`: Present when disabled, by its own prop or a `<fieldset>`.
- `data-readonly`: Present when read-only.
- `data-required`: Present when required.
- `data-unchecked`: Present while off.

## API Reference

Props are the same as below, and callbacks are events.

### Switch

- `checked?` (`boolean`): Whether it's checked. Pass it to control the checkbox.
- `defaultChecked?` (`boolean`): Whether it's checked at first, when uncontrolled. A form reset returns to it.
- `@checked-change` (`((checked: boolean, details: ChangeDetails<CheckboxCheckedChangeReason>) => void)`): Runs when a user or a form reset wants to change it. `details.reason` says why, and `details.cancel()` stops it.
- `disabled?` (`boolean`): Disables it like a native control: it can't be focused or changed, and isn't submitted.
- `readOnly?` (`boolean`): Keeps it focusable and submitted, but stops changes.
- `required?` (`boolean`): Blocks form submission until it's checked, with the browser's own message.
- `name?` (`string`): The name it submits under. Without one, it submits nothing.
- `value?` (`string`): What it submits while checked.
- `form?` (`string`): The id of a form to belong to, when it isn't inside that form.
- `controller?` (`{ toggle: (details?: Details) => boolean; getChecked: () => boolean; setChecked: (checked: boolean, details?: Details) => boolean; focus: () => void; }`): Toggles it from anywhere. Create one with `createSwitchController()`.
- `debug?` (`boolean`): Logs every state change with its reason to the console, in development builds.

### State

What render slots and the default slot receive. The thumb's state, `SwitchThumbState`, is the same.

- `checked` (`boolean`): Whether it's checked. False while indeterminate.
- `disabled` (`boolean`): Whether it's disabled, by itself or by a `<fieldset>`.
- `readOnly` (`boolean`): Whether it's read-only: it stays focusable and submitted, but presses don't change it.
- `required` (`boolean`): Whether a form won't submit until it's checked. Indeterminate doesn't count.

### Reasons

`details.reason` says why the switch turned on or off.

- `press` (`MouseEvent | KeyboardEvent`): A single-element control was pressed: a checkbox, a switch or a toggle.
- `reset` (`none`): A form reset.
- `restore` (`none`): The browser restored state, like after going back, or a saved value was read back from storage.
- `imperative` (`none`): A controller or `api` call, or a property set on a web component.

### Controller

Create one with `createSwitchController()` and pass it as `controller`.

Every write method takes an optional `{ reason, event }`, runs the change callback like a press, and returns `false` when the change is canceled.

- `toggle(details?)` (`(details?: CheckboxChangeOptions | undefined) => boolean`): Turns it on, or off.
- `getChecked()` (`() => boolean`): Whether it's on.
- `setChecked(checked, details?)` (`(checked: boolean, details?: CheckboxChangeOptions | undefined) => boolean`): Turns it on or off, as a user would.
- `focus()` (`() => void`): Moves focus to it.

### Events

Events get the new state first, then the details. `v-model` only updates when the change event wasn't canceled.

| Binding           | On       | Receives                                                        |
| ----------------- | -------- | --------------------------------------------------------------- |
| `v-model:checked` | `Switch` | Whether the switch is on                                        |
| `@checked-change` | `Switch` | `(checked, details)`: the new state, and the [reason](#reasons) |

## Accessibility

The switch follows the [WAI-ARIA switch pattern](https://www.w3.org/WAI/ARIA/apg/patterns/switch/). `Switch` renders a button with the `switch` role, and a hidden native checkbox that takes part in forms.

### Keyboard

| Key   | Does                                   |
| ----- | -------------------------------------- |
| Tab   | Moves to the next switch               |
| Space | Turns it on or off                     |
| Enter | Submits its form, and never toggles it |

### Tested Behavior

Every line below is an automated test.

- Starts in its default state.
- Clicking toggles it.
- Clicking its label toggles it.
- Space toggles it.
- Enter submits the form and never toggles it.
- The thumb follows the state.

**The first input on a page**

- The first Space reports keyboard.

**On a touch screen**

- The first tap reports touch.

Runs in React, Vue and web components.

In forms:

- The form submits every checked control's name and value.
- Unchecked controls submit nothing.
- A checkbox outside the form joins it with the form attribute.
- Required controls block submission.
- A form reset returns every control to its default.
- A reset returns to the default value, not to nothing.
- A disabled fieldset disables the controls inside it.
- A read-only control still submits.
- A user's change fires native input and change events on the form.

Runs in React, Vue and web components.

Beyond the pattern's baseline:

**Enhancements**

- The first invalid control takes focus in document order.
- A later required field without an error part doesn't take focus from the first.

**Form-associated custom elements**

- Take part in the form with no hidden input. Web components only.
- Report validity like native controls. Web components only.
- Have live checked and value properties. Web components only.
- Are named by their labels. Web components only.

Runs in React, Vue and web components.

Screen readers hear the same thing in each tested framework, recorded by a virtual screen reader:

- Announcements while checking boxes with a keyboard.
- Announcements while choosing a radio with a keyboard.

Runs in React, Vue and web components.
