---
title: "Switch"
description: "Turns one setting on or off, taking effect at once."
url: "https://tanrei.chnotchy.com/components/switch/"
last_updated: 2026-10-23T21:17:12.000Z
---

# Switch

> Turns one setting on or off, taking effect at once.

## Overview

> [!NOTE]
> **A revised spec, not yet on every platform**
>
> This page describes the switch as the spec has it now: a 52 × 32 track, a handle that grows under a press, glyphs only from the icon set, a handle that follows a drag, and handle glyphs with a heavier weight of their own. The web component already does all of this, and so do the demos. SwiftUI, Compose, Flutter and React Native are marked lagging above: until they catch up they ship the previous revision, which answers a tap only and follows no drag.

### Usage

**Web**

```html
<tanrei-switch label="Wi-Fi"></tanrei-switch>
<tanrei-switch checked label="Bluetooth"></tanrei-switch>

<!-- A glyph in the handle: a name in the icon set, for on and, if you want one, for off. -->
<tanrei-switch checked checked-icon="check" unchecked-icon="close" label="Airplane mode"></tanrei-switch>

<script type="module">
  import '@tanrei/web/components/switch';
  // The element switches its own checked before it fires change.
  document.querySelector('tanrei-switch').addEventListener('change', (e) => setWifi(e.detail.checked));
</script>
```

**React**

```tsx
import { Switch } from '@tanrei/react';

<Switch checked={wifi} label="Wi-Fi" onChange={(e) => setWifi(e.detail.checked)} />
<Switch checked={airplane} checkedIcon="check" label="Airplane mode" onChange={(e) => setAirplane(e.detail.checked)} />
```

**SwiftUI**

```swift
import Tanrei

TanreiSwitch("Wi-Fi", isOn: $wifi)
TanreiSwitch("Bluetooth", isOn: $bluetooth).disabled(!canUseBluetooth)
```

**Compose**

```kotlin
import io.github.chnotchy.tanrei.components.TanreiSwitch
import io.github.chnotchy.tanrei.primitives.TanreiText

TanreiSwitch(checked = wifi, onCheckedChange = { wifi = it }, label = "Wi-Fi")

// thumbContent is the glyph for the current position, or null for none.
TanreiSwitch(
    checked = airplane,
    onCheckedChange = { airplane = it },
    label = "Airplane mode",
    thumbContent = if (airplane) { { TanreiText("check") } } else null,
)
```

**Flutter**

```dart
import 'package:tanrei/tanrei.dart';

TanreiSwitch(
  value: wifi,
  onChanged: (bool value) => setState(() => wifi = value), // null disables it
  label: 'Wi-Fi',
);

// thumbIcon is resolved with WidgetState.selected while the switch is on.
TanreiSwitch(
  value: airplane,
  onChanged: (bool value) => setState(() => airplane = value),
  thumbIcon: WidgetStateProperty.resolveWith(
    (Set<WidgetState> states) => states.contains(WidgetState.selected) ? const Icon(AppSymbols.check) : null,
  ),
  label: 'Airplane mode',
);
```

**React Native**

```tsx
import { Switch } from '@tanrei/react-native';

<Switch value={wifi} onValueChange={setWifi} label="Wi-Fi" />
<Switch value={airplane} onValueChange={setAirplane} selectedIcon="check" label="Airplane mode" />
```

## Specs

The handle is a circle in every condition, with or without a glyph. A tap switches it, and the handle slides to the other end while its size and every color that differs between off and on change with it. A drag along the track carries the handle with the finger and draws the same in-between looks, with no threshold. The switch changes when the drag is released: to the side the handle is nearer to, or the side a fling goes toward. A drag that is cancelled, by Escape or by the platform, changes nothing.

- Category: Selection
- Level: base
- Listing: default
- Page: https://tanrei.chnotchy.com/components/switch/
- Spec: https://github.com/chnotchy/tanrei/blob/main/specs/switch.spec.json

### Availability

| Design system | Has it | Notes |
| --- | --- | --- |
| Tanrei (`tanrei`) | Yes | — |
| Cupertino (Apple HIG-inspired) (`cupertino`) | Yes | — |
| M3 Expressive-like (Material 3 Expressive-inspired) (`m3-expressive-like`) | Yes | — |
| M3-like (Material 3-inspired) (`m3-like`) | Yes | — |

### Platforms

- Web: the element `<tanrei-switch>`, registered by importing `@tanrei/web` (or only its own module, `@tanrei/web/components/switch`).
- React: `Switch` from `@tanrei/react`.

| Platform | Name | Status |
| --- | --- | --- |
| Web | `tanrei-switch` | lagging |
| SwiftUI | `TanreiSwitch` | lagging |
| Jetpack Compose | `TanreiSwitch` | lagging |
| Flutter | `TanreiSwitch` | lagging |
| React Native | `Switch` | lagging |

Names a platform gives in its own idiom:

| Spec | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
| --- | --- | --- | --- | --- | --- |
| `checked` (prop) | `checked` | `isOn` | `checked` | `value` | `value` |
| `disabled` (prop) | `disabled` | `disabled` | `enabled` | `onChanged` | `disabled` |
| `label` (prop) | `label` | `title` | `label` | `label` | `label` |
| `labelHidden` (prop) | `labelHidden` | `labelsHidden` | `labelHidden` | `labelHidden` | `labelHidden` |
| `checkedIcon` (prop) | `checkedIcon` | `selectedIcon` | `thumbContent` | `thumbIcon` | `selectedIcon` |
| `uncheckedIcon` (prop) | `uncheckedIcon` | `unselectedIcon` | `thumbContent` | `thumbIcon` | `unselectedIcon` |
| `change` (event) | `change` | `isOn` | `onCheckedChange` | `onChanged` | `onValueChange` |

Statuses: `planned` — In the inventory. Nothing shippable exists yet. `lagging` — An implementation of an earlier spec exists; it has not caught up with the current one. `alpha` — Implemented with tests. The API may still change. `beta` — Feature complete and at parity. Only fixes are expected. `stable` — Covered by semantic versioning. `deprecated` — Still shipped; scheduled for removal.

### Anatomy

- `track`: The pill the handle travels along (Material Design 3's 52 × 32); filled to show on. Unlike the track of the tabs, which never changes, it differs between off and on: its fill, and its outline.
- `outline`: The border of the track (Material Design 3's 2), drawn inside its edge. The fill sits inside the border, so a translucent fill and border never overlap.
- `handle`: The thumb: a circle in every condition and state, with or without a glyph, that sits at the inline start when off and at the inline end when on, and follows a finger dragged along the track, its size and color changing continuously on the way. Its size is its condition's in the current state (Material Design 3's 16 off, 24 on or with a glyph, 28 under a press); its center never leaves the track's midline.
- `container`: The round state layer centered on the handle (Material Design 3's 40), moving with it. It carries hover, focus and press and is invisible at rest.
- `icon` (optional): A glyph of the system's icon set inside the handle that says on or off (check and close are the usual ones), drawn with the icon set's family, fill and optical size at the glyph's own weight, comp.switch.icon.weight: in the default system heavier than the system's icons, because it is the smallest glyph the system draws and sits on a handle little larger than itself. The switch draws no mark of its own; a system that wants another mark chooses another icon set. The glyph is decorative: on and off are announced from the state.
- `label` (optional): The visible text that names the setting. A click on it toggles the switch.
- `focus-ring`: Gap-then-ring indicator shown for keyboard focus, around the track.

### Props

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `checked` | `boolean` | `false` | `checked` | Whether the setting is on. |
| `disabled` | `boolean` | `false` | `disabled` | Prevents interaction and removes the switch from the focus order. |
| `readOnly` | `boolean` | `false` | `readonly` | Shows the setting and keeps it: the switch can be focused and read and is submitted with its form, but the person cannot switch it or drag its handle. Drawn as an enabled switch, without the hover and press answers (SWT-25). |
| `label` | `string` | `""` | `label` | The text that names the setting: drawn next to the track and used as the accessible name. |
| `labelHidden` | `boolean` | `false` | `label-hidden` | Draws no visible label but keeps label as the accessible name (a switch in a list row whose headline already names it). |
| `checkedIcon` | `string` | `""` | `checked-icon` | The glyph (a name in the system's icon set) the handle shows while on. Empty shows none. |
| `uncheckedIcon` | `string` | `""` | `unchecked-icon` | The glyph the handle shows while off. Empty shows none; a product that marks only the on position leaves it empty. |

### Events

| Event | Description |
| --- | --- |
| `change` | The user switched it; carries the new value of checked. |

### States

`enabled`, `disabled`, `hovered`, `focused`, `pressed`, `selected`

### Accessibility

- Role: `switch`
- Labeling: label is the accessible name, visible or not (labelHidden). A click on the visible label toggles the switch. The icons are decorative: on and off are announced from the state, never from the glyph.
- Minimum target size: yes

| Keys | Action |
| --- | --- |
| Tab | Moves focus to the switch. |
| Space | Switches it; nothing while it is read-only. |
| Enter | Nothing on the web (it submits the form); switches it where the platform activates any focused control with Enter (Android, tvOS). |
| Escape (during a drag) | Cancels a drag that a pointer has in progress; the handle returns and the switch does not change. |

- The state is announced as on / off: role switch with aria-checked on the web, the switch trait of a toggle on Apple platforms, Role.Switch with a toggleable state on Android, Semantics toggled in Flutter, accessibilityRole "switch" with accessibilityState checked in React Native.
- Read-only (SWT-25) is a switch that shows a setting in place and keeps it in the focus order, so that it is read with the others — a setting a policy holds, or one only an administrator may change. HTML's readonly attribute does not apply to a checkbox, the switch's native element on the web, so the switch cancels its activation itself and says read-only with aria-readonly, which WAI-ARIA allows on the switch role; React Aria's isReadOnly is the same: focusable, announced with its value, not changeable. Where the person may not reach it at all, it is disabled.
- A tap anywhere on the switch or its label toggles it. A drag along the track carries the handle with the finger, and the switch takes the side the handle is released on (SWT-19 to SWT-21).
- The drag is never the only way (WCAG 2.5.7): a single tap, a click on the label and Space all switch it. With a screen reader running, a one-finger drag belongs to the reader; the switch is then toggled by the reader's activation, as before.
- A drag changes what is drawn, continuously, and nothing that assistive technology is told: between its ends the switch looks partly on, but the state stays on or off as it was, and is announced once, when the release changes it. There is no threshold in the look; the middle of the travel decides only what a release chooses.
- The switch and the tabs share one model: a thumb on a track that slides to what is chosen and follows a finger. A switch has two states that are not equals, so its track, outline and handle differ between off and on; tabs have two or more equal choices, so their track never changes.
- The glyph in the handle is drawn at its own weight (comp.switch.icon.weight, SWT-12), heavier than the system's icons in the default system. It is decoration: on and off are told by the handle's place, the track's fill and the announced state, never by the glyph alone, so no contrast criterion rests on it. The off glyph is the fainter of the two by color (a light glyph on a mid-tone handle in the default system); the weight makes its stroke wider, it does not change that color.
- The label wraps at 200% text size; the track does not grow with text.
- How a state looks is a value decision of the active design system; components read the state's tokens and never compute the effect themselves.

### Behaviour

Requirements in EARS form, each with its stable id.

- **SWT-1** WHEN the user activates the switch by pointer, touch, Space, assistive technology or a click on its label THE switch SHALL switch checked and emit change exactly once with the new value; Enter SHALL activate it only on a platform whose convention activates any focused control with Enter (Android, tvOS), and SHALL NOT on the web, where Enter submits the form.
- **SWT-2** WHILE disabled is true THE switch SHALL NOT change or emit change, SHALL NOT be focusable, SHALL ignore clicks on its label and SHALL expose the disabled state to assistive technology.
- **SWT-3** WHILE hovered, keyboard-focused or pressed THE switch SHALL render its container, track, outline, handle (color, size and elevation), icon, label and opacity from the component tokens of its condition (selected or unselected) in the highest-precedence active state, with precedence pressed over focused over hovered; focus gained by pointer or touch SHALL NOT select the focused state. A part the interaction states recolor changes over the component's state motion, as the container does; only a change of condition uses the selection motion.
- **SWT-4** WHEN the switch receives keyboard focus THE switch SHALL show the focus ring around its track without animation; focus gained by pointer or touch SHALL NOT show it.
- **SWT-5** WHERE the visual size is below the minimum target THE switch SHALL extend its hit area on both axes to sys.target.touch for touch input and sys.target.pointer for fine pointers, without changing its visual size; a platform that cannot tell the input type SHALL use sys.target.touch. The whole track and the visible label SHALL be part of the hit area.
- **SWT-6** WHILE disabled is true THE switch SHALL render its container, track, outline, handle, icon, label and opacity from the disabled component tokens of its condition.
- **SWT-7** WHEN the switch is pressed by pointer, touch or Space, or by Enter where Enter activates it (Android, tvOS; never on the web, where Enter submits the form) THE switch SHALL show the pressed state without delay and SHALL keep it visible for at least sys.motion.duration.press-hold, even when the press ends sooner.
- **SWT-8** WHILE pressed THE switch SHALL answer as sys.behavior.pressed-feedback names: overlay applies the pressed tokens to the whole container at once; ripple grows the pressed state layer from the point of contact (from the center for keyboard activation; a press outside the container (on the label) grows it from the container's center); dim applies the pressed opacity to the whole control and draws no state layer; none draws no answer to the press. Under ripple and dim only the container color stays with the state under the press; track, outline, handle, icon, label, opacity and shape still come from the pressed tokens. Under none every part stays with the state under the press. The ripple grows over comp.switch.motion.feedback-duration with motion.state-easing, fades over motion.state-duration with motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced.
- **SWT-9** THE switch SHALL ease its container and track corners by sys.shape.corner-smoothing (0 is a quarter circle, 0.6 the iOS continuous corner; a platform with only the OS curve draws it for any amount above 0) and show its keyboard focus as sys.behavior.focus-style names (ring or glow); a platform that cannot draw a choice SHALL fall back to the first value and state that in its README.
- **SWT-10** THE switch SHALL draw a track of comp.switch.track.width by comp.switch.track.height with corners comp.switch.track.shape, its outline of width comp.switch.outline.width inside the track's edge and its fill inside the outline, never under it; THE switch SHALL draw the handle as a circle whose diameter is its size, with or without an icon, its center on the track's midline, half the track height from the inline-start end while unselected and from the inline-end end while selected, and the round container of diameter comp.switch.container.size SHALL be centered on the handle and may extend beyond the track without affecting layout.
- **SWT-11** WHERE the layout direction is right-to-left THE switch SHALL mirror the handle's positions (off at the right end, on at the left) and the direction it travels, and SHALL place the label mirrored as well; a drag (SWT-19) SHALL still carry the handle to where the pointer is.
- **SWT-12** WHILE the handle shows an icon THE switch SHALL draw the handle at the larger of its condition's handle size and comp.switch.handle.size-with-icon, and SHALL draw the glyph of the system's icon set that the prop names, centered in the handle at comp.switch.icon.size, with the family and fill of its condition (selected or unselected), the weight comp.switch.icon.weight (not sys.icon.weight) in both conditions, and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max; the handle shows checkedIcon while selected and uncheckedIcon while unselected, and no icon when that prop is empty. THE switch SHALL draw no mark of its own (no path, shape or character of its own for a check or a close mark). The weight SHALL change the stroke of the glyph only: the glyph box stays comp.switch.icon.size, and the handle, the track and the place of the glyph are the same at every weight.
- **SWT-13** WHEN checked changes THE switch SHALL move the handle along the track, change its size, and change the track, outline, handle and icon colors over comp.switch.motion.selection-duration with comp.switch.motion.selection-easing, every part passing through the in-between looks of SWT-19: at each moment the switch is drawn as a drag would draw it with the handle at that place; WHERE motion is reduced it SHALL show the new condition at once.
- **SWT-14** THE switch SHALL expose the role switch with the state on or off, never as a checked checkbox where the platform tells the two apart, and SHALL NOT expose the icon to assistive technology.
- **SWT-15** WHILE label is not empty and labelHidden is false THE switch SHALL draw label at the inline end of the track, comp.switch.label.gap away, in comp.switch.label.typescale, wrapping rather than truncating, the control centered on the label's first line.
- **SWT-16** WHILE labelHidden is true THE switch SHALL draw no label and SHALL still expose label as its accessible name; WHILE label is not empty, or a Field that frames it gives it a label, THE switch SHALL expose that label as its accessible name, over a name the product gives it on the element (aria-label or aria-labelledby); WHILE label is empty and the product gives the switch a name of its own on the element — aria-label, or aria-labelledby naming an element — THE switch SHALL expose that name as its accessible name; WHILE label is empty and the product gives it none THE switch SHALL still render, and in debug builds report that it has no accessible name.
- **SWT-17** IF the icon set cannot draw the glyph yet (its font is still loading or failed to load) THE switch SHALL draw nothing in the glyph's place, never the glyph's name as text, SHALL still draw the handle at the size it has while it shows an icon and expose its state, and SHALL show the glyph once the icon set can draw it, without the selection motion.
- **SWT-18** WHEN the pressed state begins THE switch SHALL draw the handle at its condition's pressed size at once, grown or shrunk about its center, which stays where SWT-10 puts it; WHEN the pressed state ends THE switch SHALL return the handle to the size of the state under the press over comp.switch.motion.state-duration with comp.switch.motion.state-easing, and at once where motion is reduced. A change of condition while pressed SHALL move the handle along the track at its pressed size. WHERE sys.behavior.pressed-feedback is none the handle SHALL keep the size of the state under the press (SWT-8).
- **SWT-19** WHEN the user presses the track, the handle or the hit area around them (SWT-5), but not the label, with touch, a pen or a mouse and drags past the platform's touch slop (8 points where the platform names none), further along the track than across it, THE switch SHALL take the drag: the press SHALL NOT switch it when it ends (SWT-1), the switch SHALL stay in its pressed state for as long as the drag lasts (SWT-7, SWT-18), and THE switch SHALL draw the handle's center, with the container around it, on the track's midline at the pointer's place along the track, kept between its unselected and its selected position (SWT-10), in the frame the pointer moves, without a motion of its own; WHEN the drag is taken with the pointer away from the handle's center THE switch SHALL bring the handle to the pointer's place over comp.switch.motion.state-duration with comp.switch.motion.state-easing instead of at once, following the pointer meanwhile. WHILE a drag holds the handle THE switch SHALL draw every part that differs between its two conditions at the handle's progress p, which is 0 at the unselected position, 1 at the selected position and proportional to the distance between them, continuously and with no threshold: the handle's size and elevation, and the colors of the track, the outline, the handle, the container and the label, each at the linear interpolation by p between the unselected and the selected condition's tokens of the current state; and the icon as the unselected glyph at an opacity of 1 − p drawn under the selected glyph at an opacity of p, each in its own condition's color, family and fill and both at comp.switch.icon.weight, a glyph whose prop is empty drawing nothing. The in-between look SHALL follow the handle in the same frame, without a motion of its own. A drag that is further across the track than along it when it passes the slop SHALL be left to scrolling for its whole length.
- **SWT-20** WHEN the user releases a drag the switch has taken THE switch SHALL choose a condition: the one whose position the handle's center is nearer to (the other condition from the middle of its travel on), or, WHEN the release is a fling along the track, the one the fling is toward; a fling is a release faster than the platform's minimum fling velocity (Android's ViewConfiguration.getScaledMinimumFlingVelocity, Flutter's kMinFlingVelocity) or, where the platform names none (the web, SwiftUI, React Native), 0.5 points per millisecond. A release's velocity is the pointer's over its last moves: WHERE the pointer has not moved for longer than the time after which the platform's velocity tracker takes a pointer as stopped (40 milliseconds in Android's VelocityTracker, Jetpack Compose's VelocityTracker and Flutter's VelocityTracker) or, where the platform names none (the web, SwiftUI, React Native), 40 milliseconds before the release THE switch SHALL take the release as having no velocity, so that a drag that stops where it is wanted and is let go there is no fling, however fast it moved before it stopped. WHERE the chosen condition is not the one checked holds THE switch SHALL switch checked and emit change exactly once with the new value, as an activation does (SWT-1), and SHALL bring the handle, and with it the in-between look of every part (SWT-19), from where the release left them to that condition's position and look over comp.switch.motion.selection-duration with comp.switch.motion.selection-easing; WHERE it is the one checked holds THE switch SHALL return them to it over the same motion and emit nothing. The middle decides only what a release chooses; nothing drawn changes at it. The release SHALL end the pressed state as the end of a press does (SWT-7, SWT-18) and SHALL NOT also count as an activation.
- **SWT-21** WHEN a drag the switch has taken is cancelled — the platform cancels the pointer (a system gesture, a lost capture), Escape is pressed while it lasts, disabled becomes true, or the product sets checked — THE switch SHALL return the handle and the in-between look of every part to the condition checked holds (the one the product has just set, where it set one) over comp.switch.motion.selection-duration with comp.switch.motion.selection-easing and emit nothing. WHILE a drag lasts THE switch SHALL NOT change checked, the value it would submit with a form, or the state it exposes to assistive technology (SWT-14), whatever it draws, and SHALL announce nothing: the switch changes, and says so, on the release alone (SWT-20). WHERE motion is reduced THE switch SHALL still draw the handle where the pointer is and every part at the handle's progress (SWT-19), which the person's own finger moves, taking the drag without the catch-up motion, and SHALL show the chosen condition at once when the drag is released or cancelled (SWT-13).
- **SWT-22** THE switch SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the switch is shown THE switch SHALL draw itself for the new direction at once, without a motion of its own, keeping its selection and its focus.
- **SWT-23** WHERE the layout direction is right-to-left THE switch SHALL draw the checkedIcon and uncheckedIcon glyphs in its handle mirrored left to right exactly when the system's icon set marks that glyph's name as mirrored or the product forces mirroring for that use, and unmirrored when the product forbids it: the icon helper mirrors the glyph inside its own box, about the box's vertical centre line, changing neither its place, size, color and fill nor the order SWT-11 gives. THE switch SHALL NOT mirror a glyph itself, and SHALL NOT mirror content the product gives as an image or a view of its own rather than by a glyph's name.
- **SWT-24** THE switch SHALL lay out its label as text of its own direction, isolated from the text around it: each takes the base direction of its first strong character, or the layout direction where it has none, so that a left-to-right word, a number with its sign or unit, a telephone number and an address keep their own order in a right-to-left layout and reorder nothing beside them; THE switch SHALL still place the label and align its lines by the layout direction, not by the text's own.
- **SWT-25** WHILE readOnly is true and disabled is false THE switch SHALL stay focusable and in the focus order; SHALL NOT change checked, and SHALL NOT emit change, on any activation — pointer, touch, Space, Enter where Enter activates it, assistive technology, a click on its label or a click dispatched on the element; SHALL NOT take a drag (SWT-19) nor change its handle's size for a press (SWT-18); SHALL expose the read-only state to assistive technology beside its on or off state; WHERE the platform has forms SHALL be submitted with its form as it is drawn; and SHALL be drawn from the resting component tokens of its condition — the enabled ones, never the disabled ones — and SHALL show no hovered and no pressed state, under any pressed feedback (no overlay, no ripple, no dim), while keyboard focus still selects the focused state and shows the focus ring (SWT-3, SWT-4). The product may still set checked, which then moves the handle (SWT-13). WHILE disabled is true as well, disabled wins (SWT-2).
- **SWT-26** WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE switch SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on the element that carries its role — an id reference by the elements it names in the host's tree or a tree around it, an error message as part of the description while the switch is invalid — except those the switch owns (aria-checked, aria-pressed, aria-disabled), which it SHALL move and not expose. WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE switch SHALL take it off that element. WHEN the product sets an aria- attribute the switch owns THE switch SHALL report it in debug builds.

### Tokens

As written in the token documents (DTCG): an alias in braces names the token it takes its value from, which each design system resolves. On the web each token is a CSS custom property: `--tanrei` and the name after `tanrei`, joined by hyphens (`tanrei.comp.switch.…` → `--tanrei-comp-switch-…`).

#### `tanrei.comp.switch`

| Token | Type | Value |
| --- | --- | --- |
| `comp.switch.container.shape` | `dimension` | `{tanrei.sys.shape.corner.full}` |
| `comp.switch.container.size` | `dimension` | `40px` |
| `comp.switch.track.width` | `dimension` | `52px` |
| `comp.switch.track.height` | `dimension` | `32px` |
| `comp.switch.track.shape` | `dimension` | `{tanrei.sys.shape.corner.full}` |
| `comp.switch.outline.width` | `dimension` | `{tanrei.sys.border.thick}` |
| `comp.switch.handle.size-with-icon` | `dimension` | `24px` |
| `comp.switch.icon.size` | `dimension` | `{tanrei.sys.icon.extra-small}` |
| `comp.switch.icon.weight` | `number` | `{tanrei.sys.icon.mark.weight}` |
| `comp.switch.label.typescale` | `typography` | `{tanrei.sys.typescale.body}` |
| `comp.switch.label.gap` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.switch.unselected.container.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.switch.unselected.track.color` | `color` | `{tanrei.sys.color.surface-container-highest}` |
| `comp.switch.unselected.outline.color` | `color` | `{tanrei.sys.color.outline}` |
| `comp.switch.unselected.handle.color` | `color` | `{tanrei.sys.color.outline}` |
| `comp.switch.unselected.handle.size` | `dimension` | `16px` |
| `comp.switch.unselected.handle.elevation` | `shadow` | `{tanrei.sys.elevation.level0}` |
| `comp.switch.unselected.icon.color` | `color` | `{tanrei.sys.color.surface-container-highest}` |
| `comp.switch.unselected.label.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.switch.unselected.state-layer.color` | `color` | `{tanrei.comp.switch.unselected.outline.color}` |
| `comp.switch.selected.container.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.switch.selected.track.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.switch.selected.outline.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.switch.selected.handle.color` | `color` | `{tanrei.sys.color.on-primary}` |
| `comp.switch.selected.handle.size` | `dimension` | `24px` |
| `comp.switch.selected.handle.elevation` | `shadow` | `{tanrei.sys.elevation.level0}` |
| `comp.switch.selected.icon.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.switch.selected.label.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.switch.selected.state-layer.color` | `color` | `{tanrei.comp.switch.selected.outline.color}` |
| `comp.switch.motion.state-duration` | `duration` | `{tanrei.sys.motion.role.state.compact}` |
| `comp.switch.motion.state-easing` | `cubicBezier` | `{tanrei.sys.motion.easing.state}` |
| `comp.switch.motion.feedback-duration` | `duration` | `{tanrei.sys.motion.role.feedback}` |
| `comp.switch.motion.selection-duration` | `duration` | `{tanrei.sys.motion.role.selection.travel}` |
| `comp.switch.motion.selection-easing` | `cubicBezier` | `{tanrei.sys.motion.easing.spatial}` |

Per-state tokens, valued by the design system's pattern rules (not written by hand):

- `comp.switch.<variant>.<state>.<property>` for the variants `unselected`, `selected`, the states `hovered`, `focused`, `pressed`, `disabled` and the properties `container.color`, `track.color`, `outline.color`, `handle.color`, `handle.size`, `handle.elevation`, `icon.color`, `label.color`, `state-layer.color`.
- `comp.switch.<state>.<property>` for the states `hovered`, `focused`, `pressed`, `disabled` and the properties `opacity`, `container.shape`.

## Guidelines

### When to use

Use a switch for one setting that applies as soon as it changes: Wi-Fi, notifications for a conversation, a dark theme. The person sees the result without pressing a save button. When the choice only takes effect after the person confirms a form, use a checkbox instead.

### Checkbox, radio button or switch

| Control | Choices | Takes effect | Use for |
| --- | --- | --- | --- |
| [Checkbox](https://tanrei.chnotchy.com/components/checkbox/) | Any number of independent choices, including none | When the person confirms | Options in a form, agreeing to terms, selecting rows |
| [Radio button](https://tanrei.chnotchy.com/components/radio-button/) | Exactly one from a small group, all shown | When the person confirms, usually | One answer to one question |
| Switch | One setting, on or off | At once | Settings that apply as soon as they change |

### Icons in the handle

`checkedIcon` and `uncheckedIcon` name a glyph in the system’s icon set for the handle, which makes the two positions easier to tell apart at a glance. A check for on is the common choice; leave `uncheckedIcon` empty to mark only the on position. The icons are decoration: the state is announced from the switch itself, never from the glyph.

The glyph is always the icon set’s, drawn in the family and fill of the switch’s position. It has a weight of its own, heavier than the system’s other icons (500 in the default system, where the icons are 300), because at the handle’s size a stroke of the icons’ weight is under one pixel wide. Its size does not change, and a preset keeps its own icon weight. The switch draws no check or cross of its own; a product that wants a different mark chooses a different icon set. While the icon font is still loading, the handle keeps the size it has with a glyph and shows nothing in its place, never the glyph’s name as text. The glyph appears once the font can draw it.

### Size and press

In the default system the switch has a 52 × 32 track with a 2 outline, and a handle of 16 while off and 24 while on or with a glyph. A press grows the handle at once to fill the track, in either position. Once the press ends it returns to its size over the state motion, or at once where motion is reduced. A system that answers a press with nothing keeps the handle’s size.

### The same thumb as tabs

The switch and [tabs](https://tanrei.chnotchy.com/components/tabs/) share one model: a thumb on a track that slides to what is chosen and follows a finger. A switch has two states that are not equals, so its track, outline and handle differ between off and on. Tabs offer two or more equal choices, so their track never changes. Use a switch for a setting that is on or off, and tabs to choose among views.

### Read-only

A read-only switch shows a setting that the person can read but not change, such as one held by a policy or one only an administrator may change. It stays in the focus order, is announced with its state and as read-only, and is submitted with its form. It looks like an enabled switch without the hover and press feedback, and its handle cannot be dragged. Say near it why it cannot be changed. Use `disabled` when the setting does not apply at all.

### Do

- Name the setting in the label, not the action: “Wi-Fi”, not “Turn on Wi-Fi”.
- Apply the change as soon as the switch moves. If the setting cannot be applied, move the switch back and say why in text next to it.
- In a list row whose headline already names the setting, hide the label with `labelHidden` and keep it as the accessible name.

### Don’t

- Don’t use a switch in a form that is submitted later. Use a checkbox.
- Don’t use a switch to choose between two options that are not on and off, such as “List” and “Grid”. Use radio buttons.
- Don’t change the label to “On” or “Off” to show the state. The handle’s position shows it, and the platform announces it.

## Accessibility

### What the component handles

- It exposes the switch role with its state (on or off) and its disabled state, never a checked checkbox where the platform tells the two apart.
- The label is the accessible name, whether it is drawn or hidden with `labelHidden`. The drawn text and the icons are hidden from assistive technology.
- A click or tap anywhere on the track or the label toggles it. The track and the label are the hit area, and the hit area meets the minimum target.
- A drag along the track carries the handle and switches on release. It is never the only way: a tap, a click on the label and Space all switch it. With a screen reader running, a one-finger drag belongs to the reader, and the switch is toggled by the reader’s activation.
- During a drag the switch may look partly on, but its state, and the value it would submit with a form, stay as they were. The change is announced once, when the release makes it.
- Space toggles it. Enter toggles it only where the platform activates any focused control with Enter (Android, tvOS); on the web, Enter submits the form.
- While `readOnly` is set it stays focusable and is announced as read-only beside its state. No tap, drag, Space, click on the label or `click()` changes it, and it shows no hover or press feedback. On the web its value is still submitted. HTML’s `readonly` does not apply to the switch’s native element, so the component does this itself.
- In a right-to-left layout, the handle’s positions, its travel and the label are mirrored. A drag still carries the handle to where the pointer is.
- It shows the focus indicator for keyboard focus only.
- A missing label is reported in debug builds.

Known platform gaps:

- SwiftUI, Compose, Flutter and React Native ship the previous revision: the switch answers a tap and follows no drag, and its glyphs are not yet drawn at the mark’s own weight.
- `readOnly` is on the web only so far: SwiftUI, Compose, Flutter and React Native do not have it yet.

### What you need to do

- Write a label that names the setting, and keep it the same whether the switch is on or off.
- When a change fails, or applies somewhere the person cannot see, tell them in text. The switch does not announce anything beyond its own state.
