- Web
tanrei-switchschedule - SwiftUI
TanreiSwitchschedule - Jetpack Compose
TanreiSwitchschedule - Flutter
TanreiSwitchschedule - React Native
Switchschedule
生成自 docs/status/status.json, specs/switch.spec.json
规格已修订,但尚未在所有平台上实现
本页描述的是规格当前所定的开关:52 × 32 的轨道,滑块在按下时变大,图标只取自图标集,滑块跟随拖动,滑块里的图标有自己的、更粗的字重。Web 组件已经全部做到,演示也是如此。SwiftUI、Compose、Flutter 和 React Native 在上方标记为“待跟进”:在跟上之前,它们提供的是上一版,只响应轻点,不跟随拖动。
关和开
<tanrei-switch label="WLAN"></tanrei-switch>
<tanrei-switch checked label="蓝牙"></tanrei-switch>
<!-- 滑块里的图标:图标集中的一个名称,用于“开”;需要的话,也可以为“关”指定一个。 -->
<tanrei-switch checked checked-icon="check" unchecked-icon="close" label="飞行模式"></tanrei-switch>
<script type="module">
import '@tanrei/web/components/switch';
// 元素先切换自己的 checked,再触发 change。
document.querySelector('tanrei-switch').addEventListener('change', (e) => setWifi(e.detail.checked));
</script>import { Switch } from '@tanrei/react';
<Switch checked={wifi} label="WLAN" onChange={(e) => setWifi(e.detail.checked)} />
<Switch checked={airplane} checkedIcon="check" label="飞行模式" onChange={(e) => setAirplane(e.detail.checked)} />import Tanrei
TanreiSwitch("WLAN", isOn: $wifi)
TanreiSwitch("蓝牙", 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 = "WLAN")
// thumbContent 是当前位置对应的图标;为 null 则不显示图标。
TanreiSwitch(
checked = airplane,
onCheckedChange = { airplane = it },
label = "飞行模式",
thumbContent = if (airplane) { { TanreiText("check") } } else null,
)import 'package:tanrei/tanrei.dart';
TanreiSwitch(
value: wifi,
onChanged: (bool value) => setState(() => wifi = value), // 传 null 则禁用
label: 'WLAN',
);
// 开关打开时,thumbIcon 以 WidgetState.selected 解析。
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: '飞行模式',
);import { Switch } from '@tanrei/react-native';
<Switch value={wifi} onValueChange={setWifi} label="WLAN" />
<Switch value={airplane} onValueChange={setAirplane} selectedIcon="check" label="飞行模式" />位置、图标和禁用
无论处于哪种状态、有没有图标,滑块都是一个圆。轻点会切换开关,滑块滑到另一端,它的大小以及开与关之间有差别的每一种颜色也随之变化。沿轨道拖动时,滑块跟着手指移动,并画出同样的中间外观,没有阈值。开关在拖动松开时才改变:切换到滑块更靠近的一侧,或者快速滑动所指向的一侧。被 Escape 或平台取消的拖动不会改变任何东西。
构成
构成
unselected 之上。| 部件 | 说明 |
|---|---|
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 · 可选 | 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 · 可选 | 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. |
尺寸
尺寸
| 长度 | 令牌 | 值(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 之上。属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
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. |
事件
| 事件 | 说明 |
|---|---|
change | The user switched it; carries the new value of checked. |
状态
行为
以 EARS 形式写成的需求。每个平台的实现都按这些 ID 逐条测试。需求原文取自规格文件,原样以英文呈现。
| 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. |
各平台名称对照
对等并不意味着 API 完全相同。同一份契约,由各平台以自己的惯用方式表达。
| 规格 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| 组件 | tanrei-switch | TanreiSwitch | TanreiSwitch | TanreiSwitch | Switch |
checked · 属性 | checked | isOn | checked | value | value |
disabled · 属性 | disabled | disabled | enabled | onChanged | disabled |
readOnly · 属性 | readOnly | readOnly | readOnly | readOnly | readOnly |
label · 属性 | label | title | label | label | label |
labelHidden · 属性 | labelHidden | labelsHidden | labelHidden | labelHidden | labelHidden |
checkedIcon · 属性 | checkedIcon | selectedIcon | thumbContent | thumbIcon | selectedIcon |
uncheckedIcon · 属性 | uncheckedIcon | unselectedIcon | thumbContent | thumbIcon | unselectedIcon |
change · 事件 | change | isOn | onCheckedChange | onChanged | onValueChange |
| 备注 | 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. |
令牌
| 令牌 | 引用 | 解析值(浅色) |
|---|---|---|
--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 |
测试样例
作为基准的渲染结果。所有平台都以相同的 ID 为每个样例截取快照,以便相互比较。
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 生成自 specs/switch.spec.json, tokens/comp/switch.tokens.json, design/tokens/
开关用于一经更改就生效的单项设置:WLAN、某个会话的通知、深色主题。用户不用按保存按钮就能看到结果。如果选择要等用户确认表单之后才生效,请改用复选框。
| 控件 | 选项 | 何时生效 | 用途 |
|---|---|---|---|
| 复选框 | 任意数量的独立选项,也可以一项都不选 | 用户确认时 | 表单中的选项、同意条款、选择行 |
| 单选按钮 | 从全部显示出来的少量选项中恰好选一个 | 通常在用户确认时 | 一个问题的一个答案 |
| 开关 | 一项设置,开或关 | 立即 | 一经更改就生效的设置 |
checkedIcon 和 uncheckedIcon 为滑块指定系统图标集中的一个图标,让两个位置一眼就更容易分辨。常见的做法是用对勾表示“开”;uncheckedIcon 留空,就只标记“开”的位置。图标是装饰:状态由开关本身朗读,绝不根据图标朗读。
图标始终来自图标集,按开关当前位置对应的字体族和填充来绘制。它有自己的字重,比系统的其他图标更粗(在默认系统中是 500,而图标是 300),因为在滑块这个大小下,按图标的字重画出的笔画不到 1 像素宽。它的大小不变,预设沿用它自己的图标字重。开关不绘制自己的对勾或叉号;想要另一种标记的产品,应当选择另一套图标集。图标字体还在加载时,滑块保持带图标时的大小,在图标的位置上什么都不显示,绝不把图标的名称显示成文本。字体能画出图标后,图标就会出现。
在默认系统中,开关是 52 × 32 的轨道,轮廓为 2;滑块在关闭时为 16,在打开或带图标时为 24。无论在哪个位置,按下都会让滑块立即变大,填满轨道。按下结束后,它按状态动效恢复原来的大小;在减弱动态效果时,则立即恢复。对按下不作任何反馈的系统,滑块的大小保持不变。
开关和标签页共用一个模型:轨道上的一个滑块,它滑向被选中的那一项,并跟随手指。开关的两个状态并不对等,所以它的轨道、轮廓和滑块在开与关之间各不相同。标签页提供两个或更多对等的选择,所以它的轨道始终不变。开或关的设置用开关,在视图之间选择用标签页。
只读开关显示一个可以阅读但不能更改的设置,例如由策略锁定的设置,或只有管理员可以更改的设置。它留在焦点顺序中,会连同状态一起被读作只读,并随表单提交。它看起来与启用的开关相同,只是没有悬停和按下的反馈,滑块也不能拖动。请在旁边说明为什么不能更改。设置根本不适用时,请使用 disabled。
- 标签写设置的名称,而不是操作:写“WLAN”,而不是“打开 WLAN”。
- 开关一动,就应用更改。如果设置无法应用,把开关拨回去,并在它旁边用文本说明原因。
- 在标题已经说出设置名称的列表行里,用
labelHidden隐藏标签,让它继续充当无障碍名称。
- 不要在稍后才提交的表单里使用开关。请使用复选框。
- 不要用开关在两个并非“开”与“关”的选项之间选择,例如“列表”和“网格”。请使用单选按钮。
- 不要把标签改成“开”或“关”来显示状态。滑块的位置已经显示了状态,平台也会朗读它。
- 角色
switch- 标注
- 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 32px · pointer 44px · spacing 8px · touch 48px
键盘操作
| 按键 | 动作 |
|---|---|
| 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.
生成自 specs/switch.spec.json, tokens/sys/measure.tokens.json
- 它暴露开关角色及其状态(开或关)和禁用状态;在平台区分这两者的地方,绝不把它暴露成一个选中的复选框。
- 标签就是无障碍名称,无论它是画出来的,还是用
labelHidden隐藏的。画出来的文本和图标对辅助技术隐藏。 - 点击或轻点轨道或标签上的任意位置都会切换它。轨道和标签就是点击区域,点击区域满足最小目标。
- 沿轨道拖动会带着滑块移动,并在松开时切换。拖动绝不是唯一的方式:轻点、点击标签和 Space 都能切换它。屏幕阅读器运行时,单指拖动归阅读器所有,开关由阅读器的激活操作来切换。
- 拖动过程中,开关可能看起来是部分打开的,但它的状态,以及它随表单提交的值,都保持原样。变化只朗读一次,即松开手指、变化真正发生的时候。
- Space 切换它。Enter 只在平台用 Enter 激活任何获得焦点的控件时(Android、tvOS)才切换它;在 Web 上,Enter 提交表单。
- 设置
readOnly时,它仍可获得焦点,并在状态之外被读作只读。轻点、拖动、Space、点击标签或click()都不会改变它,也没有悬停和按下的反馈。在 Web 上它的值照常提交。HTML 的readonly对开关的原生元素无效,所以由组件自己完成这些。 - 在从右到左的布局中,滑块的位置、它的行程和标签都会镜像。拖动仍然把滑块带到指针所在的位置。
- 它只在键盘聚焦时显示焦点指示。
- 缺少标签会在调试构建中报告。
已知的平台差距:
- SwiftUI、Compose、Flutter 和 React Native 提供的是上一版:开关响应轻点,不跟随拖动,图标也还没有按标记自己的字重来绘制。
readOnly目前只在 Web 上提供:SwiftUI、Compose、Flutter 和 React Native 还没有。
- 写一个说明设置名称的标签,并且无论开关是开还是关,都保持不变。
- 更改失败,或者在用户看不到的地方生效时,用文本告诉用户。除了自身的状态之外,开关不会朗读任何内容。