Switch
Turns one setting on or off, taking effect at once.
- Category
- Selection
- Tokens
comp.switch- Level
- Base
- Web
tanrei-switchschedule - SwiftUI
TanreiSwitchschedule - Jetpack Compose
TanreiSwitchschedule - Flutter
TanreiSwitchschedule - React Native
Switchschedule
Generated from docs/status/status.json, specs/switch.spec.json
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.
Off and on
<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>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)} />import Tanrei
TanreiSwitch("Wi-Fi", isOn: $wifi)
TanreiSwitch("Bluetooth", isOn: $bluetooth).disabled(!canUseBluetooth)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,
)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',
);import { Switch } from '@tanrei/react-native';
<Switch value={wifi} onValueChange={setWifi} label="Wi-Fi" />
<Switch value={airplane} onValueChange={setAirplane} selectedIcon="check" label="Airplane mode" />Positions, icons and disabled
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.
Anatomy
Anatomy
unselected, as the web package draws it.| Part | Description |
|---|---|
1 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. |
2 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. |
3 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. |
4 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. |
5 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. |
6 label · optional | The visible text that names the setting. A click on it toggles the switch. |
7 focus-ring | Gap-then-ring indicator shown for keyboard focus, around the track. |
Measurements
Measurements
| Length | Token | Value (px) |
|---|---|---|
track width | tanrei.comp.switch.track.width | 52 |
track height | tanrei.comp.switch.track.height | 32 |
container size | tanrei.comp.switch.container.size | 40 |
unselected, as the web package draws it.Properties
| Property | Type | Default | Description |
|---|---|---|---|
checked | boolean | false | Whether the setting is on. |
disabled | boolean | false | Prevents interaction and removes the switch from the focus order. |
readOnly | boolean | false | 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 | "" | The text that names the setting: drawn next to the track and used as the accessible name. |
labelHidden | boolean | false | Draws no visible label but keeps label as the accessible name (a switch in a list row whose headline already names it). |
checkedIcon | string | "" | The glyph (a name in the system's icon set) the handle shows while on. Empty shows none. |
uncheckedIcon | string | "" | 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
Behavior
Requirements in EARS form. Every platform implementation is tested against these ids.
| ID | Requirement |
|---|---|
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. |
Platform name mapping
Parity does not mean identical APIs. Each platform uses its own idiom for the same contract.
| Spec | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| Component | tanrei-switch | TanreiSwitch | TanreiSwitch | TanreiSwitch | Switch |
checked · Property | checked | isOn | checked | value | value |
disabled · Property | disabled | disabled | enabled | onChanged | disabled |
readOnly · Property | readOnly | readOnly | readOnly | readOnly | readOnly |
label · Property | label | title | label | label | label |
labelHidden · Property | labelHidden | labelsHidden | labelHidden | labelHidden | labelHidden |
checkedIcon · Property | checkedIcon | selectedIcon | thumbContent | thumbIcon | selectedIcon |
uncheckedIcon · Property | uncheckedIcon | unselectedIcon | thumbContent | thumbIcon | unselectedIcon |
change · Event | change | isOn | onCheckedChange | onChanged | onValueChange |
| Notes | Form-associated: submits its value (default "on") under name while checked, as a native checkbox does; those are web attributes, not spec props. It switches its own checked before firing change (and input). Attributes are label-hidden, checked-icon and unchecked-icon. A drag (SWT-19 to SWT-21) is followed with Pointer Events on the track: setPointerCapture once the drag is taken, pointermove placing the handle, pointerup choosing the side, and pointercancel, lostpointercapture or Escape cancelling; the click that follows a taken drag is swallowed, aria-checked and the form value are written only on the release, and touch-action: pan-y on the track leaves vertical drags to the page. The in-between look is one progress from 0 to 1 in a registered custom property (@property, a <number>): every color that differs is color-mix() of the two conditions' tokens by it, the handle's size and place are calc() of it, and the two glyphs' opacities are it and its complement. A drag writes the property in each frame, and a tap transitions the same property (SWT-13), so both draw the same looks. readOnly is the readonly attribute (HTML's name; the native attribute does not apply to a checkbox, so the element cancels the activation and the drag itself and writes aria-readonly on its native checkbox). | Toggle idiom: TanreiSwitch(_ title:, isOn: Binding<Bool>), drawn by a Tanrei toggle style; activation writes the binding, which is the change event. SwiftUI's own Toggle puts its label first; this one follows the spec (label at the inline end), and a list row that wants the label first is list-item's layout. disabled follows .disabled(_:), labelsHidden follows .labelsHidden(). The drag is a DragGesture(minimumDistance:) in the toggle style, beside the tap; onChanged places the handle, onEnded reads predictedEndTranslation for the fling and writes the binding once. The look is drawn from one progress value (an Animatable modifier's animatableData): colors mixed by it, sizes interpolated, the two glyphs crossfaded; the drag sets it and the tap animates it. Until it catches up it follows no drag. | Compose idiom, as Switch: checked / onCheckedChange, and the handle's glyph as thumbContent, a slot the caller fills per selection; checkedIcon / uncheckedIcon map to it, drawn through the icon helper. enabled is the inverse flag; a null onCheckedChange draws a switch that only displays. The drag is Modifier.anchoredDraggable (or draggable) between the two positions, beside toggleable; onCheckedChange is called once, when the drag settles on the other side. The look is drawn from the anchored state's progress: lerp of each color and size, the two glyphs' alpha; a tap animates the same progress. Until it catches up it follows no drag. | Flutter idiom, as Switch: value / onChanged, and thumbIcon resolved per selection and interaction state (a WidgetStateProperty<Widget?>, so a glyph drawn as text takes the condition's icon family, as the Button's icon slot does). A null onChanged disables it. The drag is a GestureDetector's onHorizontalDragStart, onHorizontalDragUpdate and onHorizontalDragEnd (as Flutter's own Switch), DragEndDetails.primaryVelocity giving the fling; onChanged is called once, from onHorizontalDragEnd. The look is drawn from one AnimationController's value: Color.lerp and lerpDouble between the two conditions, the two glyphs' opacity; the drag sets the value and a tap animates it. Until it catches up it follows no drag. | Controlled, in React Native's value / onValueChange idiom (as its core Switch, which this does not wrap: the core one cannot draw Tanrei's tokens). The drag is a PanResponder on the track (or React Native Gesture Handler's Gesture.Pan() where the product has it), claiming the responder once the drag passes the slop along the track, with the release's vx giving the fling; onValueChange is called once, on release. The look is drawn from one Animated.Value (or a Reanimated shared value) from 0 to 1: interpolate with color output ranges (interpolateColor) and size ranges, the two glyphs' opacity; the drag sets the value and a tap animates it. Until it catches up it follows no drag. |
Tokens
| Token | Aliases | Resolved (light) |
|---|---|---|
--tanrei-comp-switch-container-shape | sys.shape.corner.full | 9999px |
--tanrei-comp-switch-container-size | — | 40px |
--tanrei-comp-switch-disabled-container-shape | — | 9999px |
--tanrei-comp-switch-disabled-opacity | — | 1 |
--tanrei-comp-switch-focused-container-shape | — | 9999px |
--tanrei-comp-switch-focused-opacity | — | 1 |
--tanrei-comp-switch-handle-size-with-icon | — | 24px |
--tanrei-comp-switch-hovered-container-shape | — | 9999px |
--tanrei-comp-switch-hovered-opacity | — | 1 |
--tanrei-comp-switch-icon-size | sys.icon.extra-small | 16px |
--tanrei-comp-switch-icon-weight | sys.icon.mark.weight | 500 |
--tanrei-comp-switch-label-gap | sys.space.space100 | 8px |
--tanrei-comp-switch-motion-feedback-duration | sys.motion.role.feedback | 450ms |
--tanrei-comp-switch-motion-selection-duration | sys.motion.role.selection.travel | 250ms |
--tanrei-comp-switch-motion-selection-easing | sys.motion.easing.spatial | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-switch-motion-state-duration | sys.motion.role.state.compact | 200ms |
--tanrei-comp-switch-motion-state-easing | sys.motion.easing.state | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-switch-outline-width | sys.border.thick | 2px |
--tanrei-comp-switch-pressed-container-shape | — | 9999px |
--tanrei-comp-switch-pressed-opacity | — | 1 |
--tanrei-comp-switch-selected-container-color | — | |
--tanrei-comp-switch-selected-disabled-container-color | — | |
--tanrei-comp-switch-selected-disabled-handle-color | — | |
--tanrei-comp-switch-selected-disabled-handle-elevation | — | |
--tanrei-comp-switch-selected-disabled-handle-size | — | 24px |
--tanrei-comp-switch-selected-disabled-icon-color | — | |
--tanrei-comp-switch-selected-disabled-label-color | — | |
--tanrei-comp-switch-selected-disabled-outline-color | — | |
--tanrei-comp-switch-selected-disabled-state-layer-color | — | |
--tanrei-comp-switch-selected-disabled-track-color | — | |
--tanrei-comp-switch-selected-focused-container-color | — | |
--tanrei-comp-switch-selected-focused-handle-color | — | |
--tanrei-comp-switch-selected-focused-handle-elevation | — | |
--tanrei-comp-switch-selected-focused-handle-size | — | 24px |
--tanrei-comp-switch-selected-focused-icon-color | — | |
--tanrei-comp-switch-selected-focused-label-color | — | |
--tanrei-comp-switch-selected-focused-outline-color | — | |
--tanrei-comp-switch-selected-focused-state-layer-color | — | |
--tanrei-comp-switch-selected-focused-track-color | — | |
--tanrei-comp-switch-selected-handle-color | sys.color.on-primary | |
--tanrei-comp-switch-selected-handle-elevation | sys.elevation.level0 | |
--tanrei-comp-switch-selected-handle-size | — | 24px |
--tanrei-comp-switch-selected-hovered-container-color | — | |
--tanrei-comp-switch-selected-hovered-handle-color | — | |
--tanrei-comp-switch-selected-hovered-handle-elevation | — | |
--tanrei-comp-switch-selected-hovered-handle-size | — | 24px |
--tanrei-comp-switch-selected-hovered-icon-color | — | |
--tanrei-comp-switch-selected-hovered-label-color | — | |
--tanrei-comp-switch-selected-hovered-outline-color | — | |
--tanrei-comp-switch-selected-hovered-state-layer-color | — | |
--tanrei-comp-switch-selected-hovered-track-color | — | |
--tanrei-comp-switch-selected-icon-color | sys.color.primary | |
--tanrei-comp-switch-selected-label-color | sys.color.on-surface | |
--tanrei-comp-switch-selected-outline-color | sys.color.primary | |
--tanrei-comp-switch-selected-pressed-container-color | — | |
--tanrei-comp-switch-selected-pressed-handle-color | — | |
--tanrei-comp-switch-selected-pressed-handle-elevation | — | |
--tanrei-comp-switch-selected-pressed-handle-size | — | 28px |
--tanrei-comp-switch-selected-pressed-icon-color | — | |
--tanrei-comp-switch-selected-pressed-label-color | — | |
--tanrei-comp-switch-selected-pressed-outline-color | — | |
--tanrei-comp-switch-selected-pressed-state-layer-color | — | |
--tanrei-comp-switch-selected-pressed-track-color | — | |
--tanrei-comp-switch-selected-state-layer-color | comp.switch.selected.outline.color | |
--tanrei-comp-switch-selected-track-color | sys.color.primary | |
--tanrei-comp-switch-track-height | — | 32px |
--tanrei-comp-switch-track-shape | sys.shape.corner.full | 9999px |
--tanrei-comp-switch-track-width | — | 52px |
--tanrei-comp-switch-unselected-container-color | — | |
--tanrei-comp-switch-unselected-disabled-container-color | — | |
--tanrei-comp-switch-unselected-disabled-handle-color | — | |
--tanrei-comp-switch-unselected-disabled-handle-elevation | — | |
--tanrei-comp-switch-unselected-disabled-handle-size | — | 16px |
--tanrei-comp-switch-unselected-disabled-icon-color | — | |
--tanrei-comp-switch-unselected-disabled-label-color | — | |
--tanrei-comp-switch-unselected-disabled-outline-color | — | |
--tanrei-comp-switch-unselected-disabled-state-layer-color | — | |
--tanrei-comp-switch-unselected-disabled-track-color | — | |
--tanrei-comp-switch-unselected-focused-container-color | — | |
--tanrei-comp-switch-unselected-focused-handle-color | — | |
--tanrei-comp-switch-unselected-focused-handle-elevation | — | |
--tanrei-comp-switch-unselected-focused-handle-size | — | 16px |
--tanrei-comp-switch-unselected-focused-icon-color | — | |
--tanrei-comp-switch-unselected-focused-label-color | — | |
--tanrei-comp-switch-unselected-focused-outline-color | — | |
--tanrei-comp-switch-unselected-focused-state-layer-color | — | |
--tanrei-comp-switch-unselected-focused-track-color | — | |
--tanrei-comp-switch-unselected-handle-color | sys.color.outline | |
--tanrei-comp-switch-unselected-handle-elevation | sys.elevation.level0 | |
--tanrei-comp-switch-unselected-handle-size | — | 16px |
--tanrei-comp-switch-unselected-hovered-container-color | — | |
--tanrei-comp-switch-unselected-hovered-handle-color | — | |
--tanrei-comp-switch-unselected-hovered-handle-elevation | — | |
--tanrei-comp-switch-unselected-hovered-handle-size | — | 16px |
--tanrei-comp-switch-unselected-hovered-icon-color | — | |
--tanrei-comp-switch-unselected-hovered-label-color | — | |
--tanrei-comp-switch-unselected-hovered-outline-color | — | |
--tanrei-comp-switch-unselected-hovered-state-layer-color | — | |
--tanrei-comp-switch-unselected-hovered-track-color | — | |
--tanrei-comp-switch-unselected-icon-color | sys.color.surface-container-highest | |
--tanrei-comp-switch-unselected-label-color | sys.color.on-surface | |
--tanrei-comp-switch-unselected-outline-color | sys.color.outline | |
--tanrei-comp-switch-unselected-pressed-container-color | — | |
--tanrei-comp-switch-unselected-pressed-handle-color | — | |
--tanrei-comp-switch-unselected-pressed-handle-elevation | — | |
--tanrei-comp-switch-unselected-pressed-handle-size | — | 28px |
--tanrei-comp-switch-unselected-pressed-icon-color | — | |
--tanrei-comp-switch-unselected-pressed-label-color | — | |
--tanrei-comp-switch-unselected-pressed-outline-color | — | |
--tanrei-comp-switch-unselected-pressed-state-layer-color | — | |
--tanrei-comp-switch-unselected-pressed-track-color | — | |
--tanrei-comp-switch-unselected-state-layer-color | comp.switch.unselected.outline.color | |
--tanrei-comp-switch-unselected-track-color | sys.color.surface-container-highest | |
--tanrei-comp-switch-label-typescale | sys.typescale.body | 400 14px/1.5714 |
Fixtures
Canonical renderings. Every platform snapshots each fixture under the same id so the results can be compared.
unselected
label: Wi-Fi selected
checked: true label: Wi-Fi disabled-unselected
disabled: true label: Wi-Fi disabled-selected
checked: true disabled: true label: Wi-Fi selected-icon
checked: true label: Wi-Fi checkedIcon: check unselected-icon
label: Wi-Fi checkedIcon: check uncheckedIcon: close label-hidden
checked: true label: Wi-Fi labelHidden: true read-only-selected
checked: true readOnly: true label: Wi-Fi Generated from specs/switch.spec.json, tokens/comp/switch.tokens.json, design/tokens/
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.
| Control | Choices | Takes effect | Use for |
|---|---|---|---|
| Checkbox | Any number of independent choices, including none | When the person confirms | Options in a form, agreeing to terms, selecting rows |
| 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 |
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.
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 switch and 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.
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.
- 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
labelHiddenand keep it as the accessible name.
- 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.
- 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
- The hit area meets the minimum target size even when the visual size is smaller. minimum 32px · pointer 44px · spacing 8px · touch 48px
Keyboard
| 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. |
Notes
- 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.
Generated from specs/switch.spec.json, tokens/sys/measure.tokens.json
- 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
readOnlyis set it stays focusable and is announced as read-only beside its state. No tap, drag, Space, click on the label orclick()changes it, and it shows no hover or press feedback. On the web its value is still submitted. HTML’sreadonlydoes 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.
readOnlyis on the web only so far: SwiftUI, Compose, Flutter and React Native do not have it yet.
- 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.