---
title: Switch
description: A control that toggles between on and off states.
category: Form
status: beta
since: 0.3.4
tags: [switch, toggle, state]
syncStatus: reviewed
verified: true
---

## Usage

:::TuffCodeBlock{lang="vue"}
---
code: |
  <script setup lang="ts">
  import { ref } from 'vue'

  const enabled = ref(true)
  </script>

  <template>
    <TuffSwitch v-model="enabled" />
  </template>
---
:::

### Toggle State
Click, Enter, or Space toggles the switch.
:::TuffDemoWrapper{demo="SwitchToggleStateDemo" code-lang="vue"}
---
code: |
  <template>
    <TuffSwitch v-model="first" />
    <TuffSwitch v-model="second" />
  </template>
---
:::

### Sizes
`size` also sets the label's font size and gap.
:::TuffDemoWrapper{demo="SwitchSizesDemo" code-lang="vue"}
---
code: |
  <template>
    <TuffSwitch v-model="enabled" label="Small" size="small" />
    <TuffSwitch v-model="enabled" label="Default" />
    <TuffSwitch v-model="enabled" label="Large" size="large" />
  </template>
---
:::

### Disabled
:::TuffDemoWrapper{demo="SwitchDisabledDemo" code-lang="vue"}
---
code: |
  <template>
    <TuffSwitch v-model="off" label="Disabled / off" disabled />
    <TuffSwitch v-model="on" label="Disabled / on" disabled />
  </template>
---
:::

### Loading
Set `loading` while a server confirms the change. The thumb stays on the old value as a spinning ring, and toggling is blocked.
:::TuffDemoWrapper{demo="SwitchLoadingDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import { ref } from 'vue'

  const enabled = ref(false)
  const committing = ref(false)

  async function commit(next: boolean) {
    committing.value = true
    try {
      await save(next)
      enabled.value = next
    }
    finally {
      committing.value = false
    }
  }
  </script>

  <template>
    <TuffSwitch :model-value="enabled" :loading="committing" label="Async commit" @change="commit" />
  </template>
---
:::

### Labels
A changing `label` crossfades; `labelPlacement` puts it before or after the track. The default slot renders as-is, without the transition.
:::TuffDemoWrapper{demo="SwitchLabelDemo" code-lang="vue"}
---
code: |
  <template>
    <TuffSwitch v-model="compact" label="Compact mode" />
    <TuffSwitch v-model="telemetry" label="Anonymous stats" label-placement="start" />
    <TuffSwitch v-model="autoSync" :label="autoSync ? 'On' : 'Off'" />
    <TuffSwitch v-model="autoSync">
      <strong>Auto sync</strong>
    </TuffSwitch>
  </template>
---
:::

### Custom Colors
Override `--tuff-switch-active-color`, `--tuff-switch-track-color`, or `--tuff-switch-thumb-color`.
:::TuffDemoWrapper{demo="SwitchCustomColorDemo" code-lang="vue"}
---
code: |
  <template>
    <TuffSwitch v-model="enabled" class="success-switch" />
  </template>

  <style scoped>
  .success-switch {
    --tuff-switch-active-color: var(--tx-color-success);
  }
  </style>
---
:::

### Settings Row
When state text sits outside the switch, render it with `TxTextTransformer` to get the same transition.
::::TuffDemoWrapper{demo="SwitchSettingsRowDemo" code-lang="vue"}
---
code: |
  <template>
    <div class="settings-row">
      <div>
        <TxTag label="Notifications" />
        <p>Receive important system and plugin alerts.</p>
      </div>
      <TxTextTransformer :text="notifications ? 'On' : 'Off'" />
      <TuffSwitch v-model="notifications" />
    </div>
  </template>
---
::::

### Best Practices

- Use a switch for settings that apply immediately; use a checkbox for values submitted with a form.
- When the server must confirm, set `loading` and write `modelValue` only on success. On failure, just clear `loading`.
- Put the control's name in `label` ("Compact mode"). Keep state text such as "On" outside, rendered with `TxTextTransformer`, so it isn't announced twice.
- Recolor through the CSS variables, not by rewriting `.tuff-switch__track` rules.

## API Reference

### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `modelValue` | `boolean` | `false` | The on/off state, bound with `v-model`. |
| `label` | `string` | - | Text beside the track; crossfades when it changes. |
| `labelPlacement` | `'start' \| 'end'` | `'end'` | Puts the label before or after the track. |
| `size` | `'small' \| 'default' \| 'large'` | `'default'` | Size; also sets the label's font size and gap. |
| `disabled` | `boolean` | `false` | Blocks toggling and removes the switch from the tab order. |
| `loading` | `boolean` | `false` | Shows a spinning ring and blocks toggling. |

### Events

| Event | Params | Description |
|------|--------|-------------|
| `update:modelValue` | `(value: boolean) => void` | Fires after a user toggle with the new value. |
| `change` | `(value: boolean) => void` | Fires together with `update:modelValue`. |

### Slots

| Slot | Props | Description |
|------|------|-------------|
| `default` | - | Custom label content; overrides `label` and renders without the transition. |

## Overview

- The root is a native `<button role="switch">` with `aria-checked`.
- It is controlled: a toggle only emits, and the switch updates once the parent writes `modelValue` back.
- `disabled` and `loading` both set the native `disabled` attribute; `loading` keeps full opacity and sets `aria-busy="true"`.
- With visible text (`label` or the slot), no `aria-label` is rendered.
- Under reduced motion, the loading ring stops spinning.

## Technologies

- The state classes `is-active`, `is-disabled`, `is-loading`, and `has-label` sit on the root; the track and thumb are `.tuff-switch__track` and `.tuff-switch__thumb`.
- Source: `packages/tuffex/packages/components/src/switch/`.

<TuffDocSourceLink />
