# Switch

An on-off switch for a setting that applies as soon as it is flipped.

Switch is a two-state control for a setting the user turns on and off. Useful
wherever the change should apply the moment it is made; for a choice that only
counts once a form is submitted, use [`Checkbox`](/components/checkbox).

::component-preview{name="SwitchPreview"}
```vue
<template>
  <div class="flex flex-col gap-3">
    <Field.Root
      v-for="setting in settings"
      :key="setting.name"
      class="flex items-center gap-3 data-[disabled=true]:cursor-not-allowed"
      :options="{ disabled: setting.disabled, name: setting.name }"
    >
      <Switch.Root v-model="setting.enabled" :class="track">
        <Switch.Thumb :class="thumb" />
      </Switch.Root>

      <Label :class="label">{{ setting.label }}</Label>
    </Field.Root>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Field, Label, Switch } from '@maas/mirror/vue'

const track = [
  'group/switch flex h-6 w-[2.625rem] items-center p-[0.0625rem]',
  'rounded-component-round border-2 border-[transparent] bg-primary-subtle',
  'transition-all duration-100 ease-[ease]',
  'data-[disabled=true]:pointer-events-none',
  'outline-4 outline-transparent focus-visible:focus-ring',
  'not-data-[disabled=true]:active:bg-primary-subtle-active',
  'data-[state=checked]:bg-accent-solid',
  'not-data-[disabled=true]:data-[state=checked]:active:bg-accent-solid-active',
  'data-[disabled=true]:bg-disabled-muted',
].join(' ')

const thumb = [
  'rounded-component-round bg-primary-light block size-[1.125rem]',
  'shadow-[0_0.0625rem_0.125rem_0_oklch(0%_0_none/0.24)]',
  'transition-all duration-100 ease-[ease]',
  'data-[state=checked]:translate-x-[1.125rem]',
  'not-data-[disabled=true]:group-active/switch:w-[1.375rem]',
  'not-data-[disabled=true]:group-active/switch:data-[state=checked]:translate-x-[0.875rem]',
  'data-[disabled=true]:bg-disabled-light',
].join(' ')

const label = [
  'type-component-xs text-surface [--mirror-label-cursor:pointer]',
  'data-[disabled=true]:text-disabled-solid',
].join(' ')

const settings = ref([
  {
    name: 'notifications',
    label: 'Email notifications',
    enabled: true,
    disabled: false,
  },
  { name: 'digest', label: 'Weekly digest', enabled: false, disabled: false },
  { name: 'beta', label: 'Beta features', enabled: false, disabled: true },
])
</script>
```
::

## Usage guidelines

- Make sure the switch has an accessible name, either through a
  [`Label`](/components/label) inside a [`Field`](/components/field) or an
  `aria-label` on the root.
- `readOnly` never reaches the hidden input, so a read-only switch still submits
  its value. Use `disabled` to leave it out of the submission.

## Anatomy

Assemble the root and its thumb.

::component-anatomy
---
parts:
  - name: Switch.Root
    required: true
    description: Renders a button with role="switch", plus a visually hidden checkbox input.
    children:
      - name: Switch.Thumb
        description: Renders a span. The moving part, mounted in both states.
---
::

```vue
<script setup lang="ts">
import { Field, Label, Switch } from '@maas/mirror/vue'
</script>

<template>
  <Field.Root :options="{ name: 'notifications' }">
    <Switch.Root v-model="enabled">
      <Switch.Thumb />
    </Switch.Root>
    <Label>Email notifications</Label>
  </Field.Root>
</template>
```

## Examples

### Controlled and uncontrolled

Leave `modelValue` out and the switch keeps its own state, seeded by
`defaultValue`. Bind `v-model` and you own it instead, which is
[decided once at mount](/components/composition#controlled-and-uncontrolled).

```vue
<template>
  <Switch.Root default-value>
    <Switch.Thumb />
  </Switch.Root>

  <Switch.Root v-model="enabled">
    <Switch.Thumb />
  </Switch.Root>
</template>

<script setup lang="ts">
const enabled = ref(false)
</script>
```

### Inside a form

The hidden input takes `name`, `value`, `required` and `disabled`, so
[the form sees it](/components/form-integration#form-participation), and a surrounding `Field` supplies each of them.

```vue
<template>
  <form>
    <Field.Root :options="{ name: 'notifications' }">
      <Switch.Root>
        <Switch.Thumb />
      </Switch.Root>
      <Label>Email notifications</Label>
    </Field.Root>
  </form>
</template>
```

A switch that applies immediately usually has no form around it. If all you want
is the value in `FormData`, set `name` on the root and skip the `Field`.

```vue
<template>
  <Switch.Root name="notifications" value="on">
    <Switch.Thumb />
  </Switch.Root>
</template>
```

An off switch submits nothing, which is the native checkbox behaviour. Where
the server needs to hear about both states, set `uncheckedValue` and a second
hidden input carries it while the switch is off.

```vue
<template>
  <Switch.Root name="notifications" unchecked-value="off" value="on">
    <Switch.Thumb />
  </Switch.Root>
</template>
```

A switch rendered outside the `<form>` it belongs to points at it by `id`
through `form`, which both hidden inputs take.

### Reaching the hidden input

The element the form submits is exposed as `input` on the component instance,
for the rare case where you need to read `validity` or call
`setCustomValidity()` on it.

```vue
<template>
  <Switch.Root ref="notifications" name="notifications" />
</template>

<script setup lang="ts">
const notifications = useTemplateRef('notifications')

function report() {
  notifications.value?.input?.reportValidity()
}
</script>
```

### Styling from state

Both parts write the same state, so the thumb positions itself from
`data-state` without a wrapper class.

```vue
<template>
  <Switch.Root class="switch">
    <Switch.Thumb class="thumb" />
  </Switch.Root>
</template>

<style>
.switch {
  display: flex;
  width: 2.5rem;
  padding: 0.125rem;
  border-radius: var(--app-dimension-radius-round);
  background: var(--app-color-surface-bg-high);
}

.switch[data-state='checked'] {
  background: var(--app-color-primary-bg-solid);
}

.thumb {
  width: 1.25rem;
  height: 1.25rem;
  border-radius: var(--app-dimension-radius-round);
  transition: translate 120ms ease;
}

.thumb[data-state='checked'] {
  translate: 1rem 0;
}
</style>
```

## API reference

**Module.** A bundled `options` object, and `useMirrorSwitch(id)` as
the programmatic API. There is no `indeterminate`: a switch has two states by
definition.

### `Switch.Root`

Renders a `<button role="switch">` plus a visually hidden
`<input type="checkbox">`, as a sibling of the button rather than a child.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: The instance ID.
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: modelValue
        description: The checked state. `v-model`.
      - label: 'boolean | undefined'
      - label: undefined
  - items:
      - label: defaultValue
        description: 'Initial state when [uncontrolled](#controlled-and-uncontrolled).'
      - label: boolean
      - label: 'false'
  - items:
      - label: options
        description: Bundles every prop below. The direct props win over it.
      - label: SwitchOptions
      - label: '{}'
  - items:
      - label: name
        description: 'Form field name; renders [the hidden input](#inside-a-form).'
      - label: string
      - label: inherited from `Field`
        plaintext: true
  - items:
      - label: form
        description: The `id` of the form, when rendered outside it.
      - label: string
      - label: undefined
  - items:
      - label: value
        description: Submitted while checked.
      - label: string
      - label: '''on'''
  - items:
      - label: uncheckedValue
        description: 'Submitted while [unchecked](#inside-a-form). Omit to submit nothing.'
      - label: string
      - label: undefined
  - items:
      - label: disabled
        description: Blocks changes.
      - label: boolean
      - label: inherited from `Field`, else `false`
        plaintext: true
  - items:
      - label: readOnly
        description: Blocks changes, keeps focus.
      - label: boolean
      - label: inherited from `Field`, else `false`
        plaintext: true
  - items:
      - label: required
        description: Marks the switch required.
      - label: boolean
      - label: inherited from `Field`, else `false`
        plaintext: true
---
::

#### Emits

::docs-table
---
columns:
  - label: Emit
  - label: Payload
rows:
  - items:
      - label: update:modelValue
        description: The checked state changes.
      - label: boolean
---
::

#### Slot props

The [field state set](/components/styling#the-field-state-set), plus the one below.

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: checked
        description: Mirrors `data-checked`.
      - label: boolean
---
::

`Switch.Root` also exposes `input`, the hidden `<input type="checkbox">`
element, on its instance.

### `Switch.Thumb`

Renders a `<span>`, mounted in both states, with no props beyond the primitive
ones.

#### Slot props

The same as the root.

### Composable

`useMirrorSwitch(id)` reaches a switch from anywhere in the app.

::docs-table
---
columns:
  - label: Key
  - label: Type
rows:
  - items:
      - label: checked
        description: The checked state.
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: disabled
        description: Mirrors `data-disabled`.
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: readOnly
        description: Mirrors `data-readonly`.
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: setChecked
        description: Sets the checked state.
      - label: '(next: boolean) => void'
        escape: true
  - items:
      - label: toggle
        description: Flips the checked state.
      - label: () => void
        escape: true
---
::

### Data attributes

Both parts write the [field state set](/components/styling#the-field-state-set), plus the ones below.

::data-attributes
::

### CSS variables

::css-variables
::

### Errors

::docs-table
---
columns:
  - label: Code
rows:
  - items:
      - label: missing_switch_context
        description: '`Switch.Thumb` rendered outside `Switch.Root`.'
---
::

## Accessibility

The rendered element takes `role="switch"` with `aria-checked`, `aria-required`
follows `required`, and a non-native switch reports `aria-disabled` rather than
leaving the tab order.

::docs-table
---
columns:
  - label: Key
  - label: Action
rows:
  - items:
      - label: Enter
      - label: Toggles, on `keydown`. Unlike [`Checkbox`](/components/checkbox), the default is not blocked.
        plaintext: true
  - items:
      - label: Space
      - label: Toggles, on `keyup`.
        plaintext: true
---
::
