跳到主要内容
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
开始使用WebReactSwiftUIJetpack ComposeFlutterReact Native预设你自己的设计系统与 AI 代理一起使用 Tanrei品牌设计令牌设计系统与定制Theme Studio颜色字体排印图标形状层级动效交互状态行为布局无障碍文字颜色令牌字阶令牌形状令牌层级令牌动效令牌状态令牌间距与度量令牌组件按钮主操作按钮分段按钮按钮组带菜单的按钮工具栏复制按钮复选框单选按钮开关纸片(Chip)菜单滑块滑块字段日历DateTimePicker字段组单选组复选框组下拉选择框下拉选择字段菜单栏文件上传文件选择器 进阶放置区文本输入框 进阶文本字段纸片字段字段组合框 进阶组合框字段数字输入框 进阶数字字段颜色输入框 进阶颜色字段一次性验证码输入框 进阶一次性验证码字段嵌入式文本输入框 进阶聊天输入栏卡片分隔线模态层 进阶对话框列表列表行底部面板侧边面板轮播代码块表格弹出框步骤折叠面板描述列表图示应用外壳页头树视图时间线图片预览调整手柄可调整大小的框滚动区域徽标进度指示器计量条数字滚动器加载指示器消息条(Snackbar)工具提示键盘快捷键标签(Tag)行内代码提示块(Callout)头像空状态标签组(Tag group)骨架屏文件项色块跑马灯文本标签页标签页视图应用栏导航栏导航抽屉搜索链接分页上一页/下一页链接步骤条面包屑目录链接卡片FAB 菜单状态Tanrei 是如何构建的参与贡献许可与致谢
首页组件单选按钮

单选按钮

让用户从同时全部显示出来的一小组选项中恰好选择一个。

content_copy以 Markdown 格式查看
类别
选择
令牌
comp.radio-button
层级
基础

概览

Web
tanrei-radio-button
schedule
SwiftUI
TanreiRadioButton
schedule
Jetpack Compose
TanreiRadioButton
schedule
Flutter
TanreiRadioButton
schedule
React Native
RadioButton
schedule

生成自 docs/status/status.json, specs/radio-button.spec.json

一个组

用法

让单选按钮采用现在的尺寸、并让圆环加粗成填充的那次修订,只改变了它的外观和动效,所以下面的示例全部适用于当前版本。Web 按规格绘制它。SwiftUI、Compose、Flutter 和 React Native 待跟进:在跟上之前,它们仍然绘制上一版的单选按钮——在 28 的状态层中画一个细线描边的 16 圆环和一个 8 的圆点——并且不在布局中预留 48 的目标高度。此后还有一处改进,目前只在 Web 上实现:圆环和填充作为一个形状来绘制。

<!-- name 相同的选项构成一个组。组的名称来自包裹它们的元素。 -->
<div role="radiogroup" aria-labelledby="frequency-label">
  <span id="frequency-label">发送频率</span>
  <tanrei-radio-button name="frequency" value="daily" label="每天"></tanrei-radio-button>
  <tanrei-radio-button name="frequency" value="weekly" checked label="每周"></tanrei-radio-button>
  <tanrei-radio-button name="frequency" value="monthly" label="每月"></tanrei-radio-button>
</div>

<script type="module">
  import '@tanrei/web/components/radio-button';
  // 只有变为选中的选项触发 change;被它取代的那个不会触发。
  document.querySelector('[role="radiogroup"]').addEventListener('change', (e) => save(e.detail.value));
</script>
import { RadioButton } from '@tanrei/react';

<div role="radiogroup" aria-labelledby="frequency-label">
  <span id="frequency-label">发送频率</span>
  {options.map(({ value, label }) => (
    <RadioButton
      key={value}
      name="frequency"
      value={value}
      label={label}
      checked={frequency === value}
      onChange={(e) => setFrequency(e.detail.value)}
    />
  ))}
</div>
import Tanrei

// selection 是一个 Binding<Frequency?>:它与某个选项的 value 相等时,该选项为选中。
TanreiRadioGroup("发送频率") {
  TanreiRadioButton("每天", value: Frequency.daily, selection: $frequency)
  TanreiRadioButton("每周", value: Frequency.weekly, selection: $frequency)
  TanreiRadioButton("每月", value: Frequency.monthly, selection: $frequency)
}
import androidx.compose.foundation.selection.selectableGroup
import io.github.chnotchy.tanrei.components.TanreiRadioButton

// 组由父级构成;每个选项的 onClick 捕获它自己的值。
Column(Modifier.selectableGroup()) {
    for ((value, label) in listOf("daily" to "每天", "weekly" to "每周", "monthly" to "每月")) {
        TanreiRadioButton(selected = frequency == value, onClick = { frequency = value }, label = label)
    }
}
import 'package:tanrei/tanrei.dart';

TanreiRadioGroup<Frequency>(
  groupValue: frequency,
  onChanged: (Frequency value) => setState(() => frequency = value),
  semanticLabel: '发送频率',
  child: Column(
    children: <Widget>[
      for (final Frequency option in Frequency.values)
        TanreiRadioButton<Frequency>(value: option, label: option.label),
    ],
  ),
);
import { RadioButton, RadioGroup } from '@tanrei/react-native';

<RadioGroup accessibilityLabel="发送频率">
  {options.map(({ value, label }) => (
    <RadioButton key={value} value={value} label={label} selected={frequency === value} onSelect={setFrequency} />
  ))}
</RadioGroup>

规格

选中、未选中和禁用

在默认系统中,单选按钮是一个描边为 2 的 20 圆环,位于直径 40 的圆形状态层之上。选中的选项不画圆点。它的圆环向内加粗到半径的一半,留在里面的圆面则用圆环的颜色以 16% 的不透明度填充:一层浅色,与圆环至少保持 3:1 的对比度。两种状态下,圆环的外径都不变。圆环和填充是一个形状:无论缩放比例或显示密度如何,也无论在变化过程的哪一帧,单选按钮背后的东西都不会从两者之间透出来。选择发生变化时,描边变粗或变细,同时圆环改变颜色,填充淡入或淡出;填充绝不从中心向外扩大。动效结束之前再次变化时,会从当前的位置折返;在减弱动态效果时,新的状态立即完整显示。

每个选项在布局中至少占 48 的高度,也就是目标的尺寸,状态层在其中居中。因此,不留间隙地堆叠起来的选项绝不会共用目标区域。想把选项排得更紧的产品,可以在自己的层里调低 option.min-height,并接受目标相互重叠。

在 m3-like 下,选中的选项保留 Material 的圆环、间隙和圆点,但圆点是淡入的,而不是从中心放大出现。在 cupertino 下,单选按钮保持较小的尺寸,并画出 Apple 的外观:细圆环里的一个浅色圆面,选中时则是围绕浅色中心的一圈着色环带。

构成

构成

绘制在 Web 包所绘的测试样例 unselected 之上。
部件 说明
1 container The round state layer behind the ring. It carries hover, focus and press and is invisible at rest.
2 ring The circle that shows there is an option. Its outer diameter never changes; its stroke is thin while unselected and thickens inward, towards the center, while selected.
3 fill · 可选 What the ring leaves inside it, less an inset, tinted while selected (translucent in the default system) and clear while unselected. Where the inset is nothing it meets the ring's inner edge with no gap: ring and fill are drawn as one shape. It never grows or shrinks on its own, and it is never what carries the selection: the thickened ring is.
4 label · 可选 The visible text that names the option. A click on it selects the option.
5 focus-ring Gap-then-ring indicator shown for keyboard focus, around the ring.

尺寸

尺寸

长度 令牌 值(px)
container size tanrei.comp.radio-button.container.size 40
ring size tanrei.comp.radio-button.ring.size 20
绘制在 Web 包所绘的测试样例 unselected 之上。

属性

属性 类型 默认值 说明
checked boolean false Whether this option is the one chosen in its group. Inside a radio group it follows the group's value.
value string "" What this option stands for: carried by change, and submitted with a form on the web.
name string "" The group this option belongs to where it does not stand in a radio group: on each platform that platform's bare way of forming a group (see platforms); the options of one group exclude each other and share one tab stop. Ignored inside a radio group, which is the group.
disabled boolean false Prevents interaction; the option is skipped by the focus order and by the arrow keys.
readOnly boolean false The person cannot select this option: it can still be focused and read, and the arrow keys still reach it, but a press, Space or an arrow key that lands on it selects nothing. Drawn as an enabled option, without the hover and press answers. A radio group that is read-only makes each of its options so (RAD-28).
error boolean false Draws the option in the error form of its condition: the ring, and the fill while selected, in the error color (a required group left unanswered, a choice that cannot be taken). A radio group or a field group in error puts every option it holds there, so set it by hand only on options grouped without one (RAD-25). It draws and nothing more: the group, not the option, is exposed as invalid, and the words that say why are the group's.
label string "" The text that names the option: drawn next to the ring and used as the accessible name.
labelHidden boolean false Draws no visible label but keeps label as the accessible name.

事件

事件 说明
change This option became selected by the user; carries its value. The option it replaced does not emit. Inside a radio group the group emits change in its place (RGP-4).

状态

行为

以 EARS 形式写成的需求。每个平台的实现都按这些 ID 逐条测试。需求原文取自规格文件,原样以英文呈现。

ID 需求
RAD-1 WHEN the user activates an unselected radio button by pointer, touch, Space, assistive technology or a click on its label THE radio button SHALL become selected and emit change exactly once with its 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.
RAD-2 WHEN the user activates a radio button that is already selected THE radio button SHALL stay selected and SHALL NOT emit change: a radio button is never cleared by its own activation.
RAD-3 WHEN a radio button becomes selected THE other radio buttons of its group SHALL become unselected, without emitting change.
RAD-4 WHEN a radio button in a group has keyboard focus and the user presses Down Arrow or Right Arrow THE group SHALL move focus to the next enabled radio button, wrapping from the last to the first, and select it; Up Arrow and Left Arrow SHALL do the same towards the previous one; in a right-to-left layout Right Arrow SHALL move to the previous and Left Arrow to the next.
RAD-5 THE group SHALL keep exactly one of its radio buttons in the tab order: the selected one, or the first enabled one while none is selected; Tab SHALL leave the group rather than move within it.
RAD-6 WHILE disabled is true THE radio button SHALL NOT change or emit change, SHALL NOT be focusable, SHALL be skipped by the arrow keys, SHALL ignore clicks on its label and SHALL expose the disabled state to assistive technology.
RAD-7 WHILE hovered, keyboard-focused or pressed THE radio button SHALL render its container, ring, fill, label and opacity from the component tokens of its condition (selected or unselected, or the error- form of either, RAD-26) 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 (RAD-15).
RAD-8 WHEN the radio button receives keyboard focus THE radio button SHALL show the focus ring around its ring without animation; focus gained by pointer or touch SHALL NOT show it.
RAD-9 WHERE the visual size is below the minimum target THE radio button 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 visible label SHALL be part of the hit area as well.
RAD-10 WHILE disabled is true THE radio button SHALL render its container, ring, fill, label and opacity from the disabled component tokens of its condition.
RAD-11 WHEN the radio button 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 radio button 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.
RAD-12 WHILE pressed THE radio button 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; ring, fill, 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.radio-button.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.
RAD-13 THE radio button SHALL ease its container 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.
RAD-14 THE radio button SHALL draw a ring of outer diameter comp.radio-button.ring.size centered in a round container of diameter comp.radio-button.container.size, the outer diameter the same in both conditions; while unselected the ring SHALL be stroked at comp.radio-button.ring.width inside that diameter, and while selected at comp.radio-button.ring.selected-width-share of the ring's radius (1 being a stroke as wide as the radius, a solid disc), or at comp.radio-button.ring.width where that is wider, the stroke growing inward from the outer diameter. It SHALL draw no dot of its own; what is drawn inside the ring is the fill (RAD-19).
RAD-15 WHEN the radio button becomes selected THE radio button SHALL, over comp.radio-button.motion.selection-duration with comp.radio-button.motion.selection-easing, thicken the ring's stroke inward from its unselected to its selected width, change the ring color from the unselected to the selected one, and run the fill's color continuously from comp.radio-button.unselected.fill.color (none in the default system) to comp.radio-button.selected.fill.color, fading the opacity in from none rather than passing through any other color; WHEN it becomes unselected it SHALL run the same change in reverse; the fill SHALL NOT grow from or shrink to the center; WHERE motion is reduced it SHALL show the new condition at once and in full, with no intermediate frame.
RAD-16 THE radio button SHALL expose the role radio with the state checked or not checked, and its position in its group and the group's size; THE group SHALL be exposed as a radio group: the radio group component, named by its label (RGP-10), or, where the options are grouped without one (RAD-25), a container whose accessible name the product gives.
RAD-17 WHILE label is not empty and labelHidden is false THE radio button SHALL draw label at the inline end of the container, comp.radio-button.label.gap away, in comp.radio-button.label.typescale, wrapping rather than truncating, the control centered on the label's first line; right-to-left layouts SHALL mirror the order.
RAD-18 WHILE labelHidden is true THE radio button 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 radio button 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 radio button a name of its own on the element — aria-label, or aria-labelledby naming an element — THE radio button SHALL expose that name as its accessible name; WHILE label is empty and the product gives it none THE radio button SHALL still render, and in debug builds report that it has no accessible name.
RAD-19 THE radio button SHALL draw the fill in both conditions: the area inside the ring's inner edge at that moment, less comp.radio-button.fill.inset all round, never under the ring, in the fill color of its condition and state (comp.radio-button.selected.fill.color or comp.radio-button.unselected.fill.color at rest, and comp.radio-button.error-selected.fill.color or comp.radio-button.error-unselected.fill.color in the error forms, RAD-26; an unselected one is none in the default system, so an unselected option shows no fill there) at comp.radio-button.fill.opacity; a fill whose diameter would be zero or less SHALL NOT be drawn.
RAD-20 WHEN the condition changes again before the selection motion of RAD-15 has finished THE radio button SHALL turn the change back from the stroke width and the ring and fill colors it has reached, and SHALL NOT jump to either end.
RAD-21 THE radio button SHALL take at least comp.radio-button.option.min-height of height in layout, the container centered in it; at its default, sys.target.touch, radio buttons stacked with no space between them keep whole targets that do not overlap, and a layer that lowers it accepts shared target area.
RAD-22 THE radio button 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 radio button is shown THE radio button SHALL draw itself for the new direction at once, without a motion of its own, keeping its selection and its focus.
RAD-23 THE radio button 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 radio button SHALL still place the label and align its lines by the layout direction, not by the text's own.
RAD-24 WHILE comp.radio-button.fill.inset is 0 (the default system's) THE radio button SHALL draw the ring and the fill as one shape: every point inside the ring's outer diameter SHALL be painted by the ring, by the fill, or, along the circle where the two meet, by both in shares that add up to the whole, so that nothing behind the radio button shows between the ring's inner edge and the fill at any zoom, display density or fractional position, or in any frame of the selection motion (RAD-15); neither SHALL be painted over the other (RAD-19), so that the fill is nowhere more or less opaque than comp.radio-button.fill.opacity makes it. WHILE comp.radio-button.fill.inset is above 0 the space it leaves between the ring and the fill SHALL show what is behind the radio button, as wide as the inset all round.
RAD-25 WHILE the radio button stands in a radio group THE radio button SHALL take its group from it and from nothing else: checked SHALL follow the radio group's value and its own value (RGP-3), name SHALL be ignored, it SHALL be disabled WHILE the radio group is (RGP-7), and its selection by the person SHALL be reported by the radio group's change and by no event of its own that a listener on the group would receive as well (RGP-4). WHERE the radio button stands in no radio group it SHALL form its group by the platform's bare idiom — on the web the radio buttons of one name in the same form, or in the same document or shadow root outside a form — and RAD-3, RAD-4, RAD-5 and RAD-16 SHALL hold among those, the group's name, its required and invalid states and its layout being the product's, or those of a field group whose kind is radio-group that the product put around them (FGR-25). A radio button in no group at all SHALL still render and be selectable, as a group of one, and in debug builds SHALL report it where the platform can tell (on the web, a radio button with neither a radio group nor a name).
RAD-26 THE radio button SHALL draw the condition its props select — selected WHILE checked is true, otherwise unselected — and the error- form of that condition (error-selected, error-unselected) WHILE it is in error: WHILE error is true, or WHILE the radio group or the field group it stands in is in error and has put it there (RGP-8, FGR-8), as a checkbox draws the error- form of its condition (CHK-4). In an error form THE radio button SHALL render its container, ring, fill, label and opacity from the component tokens of that form in every state — at rest, hovered, focused and pressed with the precedence of RAD-7, and disabled as RAD-10 says — and SHALL change nothing else: the ring's diameter and its stroke in each condition (RAD-14), the fill's inset and opacity (RAD-19), the label, the focus indication (RAD-8), the hit area (RAD-9) and the selection motion (RAD-15), which runs between the two error forms' colors, are those of the same condition out of error. WHILE disabled is true the disabled tokens of the error form apply, which in the default system are the values of the plain form's: a disabled option shows no error color, because it cannot be the answer that puts the error right. WHEN the radio button enters or leaves the error forms without a change of selection THE radio button SHALL only recolor its ring and its fill, over comp.radio-button.motion.selection-duration with comp.radio-button.motion.selection-easing, and at once WHERE motion is reduced; nothing SHALL change size or move. THE radio button SHALL expose no invalid state and take no description for the error: the radio role has no such state, the group the options form carries it and the words that say why (RAD-16, RGP-10, FGR-25), and an option that repeated them would say the error once for every arrow key. It SHALL never set error itself, and being selected SHALL NOT clear it. WHERE the options are grouped without a radio group or a field group (RAD-25) error is the product's to set, on every option of the group together with the invalid state of the group it built.
RAD-27 WHERE the platform draws with a person's own forced colors (the web's forced-colors mode, and a platform's equivalent where it has one) THE radio button SHALL draw each error form exactly as it draws the same condition out of error there — its ring in the platform's own colors, the stroke's weight carrying the selection — and SHALL show no error color, because the person's colors leave none to show: the error is carried by the words of the group's error text and by the group's invalid state (RGP-14, FGR-17).
RAD-28 WHILE readOnly is true and disabled is false, or WHILE the radio group it stands in is read-only (RGP-20), THE radio button SHALL stay focusable, keep its place in its group's single tab stop (RAD-5) and be reached by its group's arrow keys (RAD-4); SHALL NOT become selected, and SHALL NOT emit change, on any activation — pointer, touch, Space, Enter where Enter activates it, assistive technology, a click on its label, a click dispatched on the element or an arrow key that moves focus to it — so that an arrow key moves focus to it and selects nothing, and from it moves focus on as RAD-4 says, selecting the option it lands on only where that option is not read-only; 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 (RAD-7, RAD-8). The radio role has no read-only state: the group says it — the radio group (RGP-20), or, where options are grouped without one (RAD-25), the container the product gives them. The product may still set checked. WHILE disabled is true as well, disabled wins (RAD-6).
RAD-29 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 radio button 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 radio button is invalid — except those the radio button 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 radio button SHALL take it off that element. WHEN the product sets an aria- attribute the radio button owns THE radio button SHALL report it in debug builds.

各平台名称对照

对等并不意味着 API 完全相同。同一份契约,由各平台以自己的惯用方式表达。

规格 Web SwiftUI Jetpack Compose Flutter React Native
组件 tanrei-radio-button TanreiRadioButton TanreiRadioButton TanreiRadioButton RadioButton
checked · 属性 checked selection selected groupValue selected
value · 属性 value value onClick value value
name · 属性 name TanreiRadioGroup selectableGroup TanreiRadioGroup RadioGroup
disabled · 属性 disabled disabled enabled enabled disabled
readOnly · 属性 readOnly readOnly readOnly readOnly readOnly
error · 属性 error isError isError isError error
label · 属性 label title label label label
labelHidden · 属性 labelHidden labelsHidden labelHidden labelHidden labelHidden
change · 事件 change selection onClick onChanged onSelect
备注 Form-associated, with the native radio's idiom: a group is the options with one name under the same form (or the same root without a form); the options manage the arrow keys and the single tab stop among themselves. A tanrei-radio-group is the usual group: inside one the option needs no name, takes checked, its tab stop and its position from the group and submits nothing itself (RAD-25). Options grouped by name alone are wrapped by the product in an element with role radiogroup and a name. labelHidden is the label-hidden attribute. error is a reflected boolean attribute. The option asks the frame around it as a checkbox does (FieldControlController with errorCondition: true, takes: 'all'), and draws its error form while error or the frame's invalid holds (data-error on .root); it writes no aria-invalid, and in a group in error it takes no description from the group's frame — it names itself, and its group says the error (RAD-26); alone in a Field it takes its name and description from the Field as any control does. Under forced-colors: active the error forms are the plain ones (RAD-27). readOnly is the readonly attribute (HTML's name; the native attribute does not apply to a radio button, so the option cancels the activation itself and writes no ARIA state, the radio role having none). In a tanrei-radio-group it is also read-only while the group is, without its own readOnly changing. TanreiRadioButton(_ title:, value: V, selection: Binding<V?>): checked is selection == value, and activation writes value to the binding, which is the change event. SwiftUI has no radio group on iOS, so TanreiRadioGroup (a container with no tokens) carries the group's name, the arrow keys and the position announcements; it is the radio group component (radio-group), which adds the frame and holds the selection. Lagging: it draws a 16 ring with a 1 stroke and an 8 dot in a 28 state layer, not this spec's ring and dot. The error condition (isError, the error forms of RAD-26 and RAD-27, set by TanreiRadioGroup in error through the environment) is not built on this platform yet. Compose idiom, as RadioButton: the caller passes selected and an onClick that captures the option's value; the parent forms the group with Modifier.selectableGroup(), which a TanreiRadioGroup (radio-group) applies itself. enabled is the inverse flag. Lagging: it draws a 16 ring with a 1 stroke and an 8 dot in a 28 state layer, not this spec's ring and dot. The error condition (isError, the error forms of RAD-26 and RAD-27, given by a TanreiRadioGroup in error through its composition local) is not built on this platform yet. Flutter idiom: TanreiRadioButton<T>(value:) inside a TanreiRadioGroup<T>(groupValue:, onChanged:, semanticLabel:) ancestor, shaped like Flutter's RadioGroup (3.35), which the package's minimum Flutter (3.32) lacks and whose name it must not take; checked is groupValue == value. The group carries the arrow keys, the single Tab stop and the radio-group semantics; it is the radio group component (radio-group), which adds the frame. enabled is the inverse flag, as Flutter's radio has since RadioGroup replaced the per-radio onChanged. Lagging: it draws a 16 ring with a 1 stroke and an 8 dot in a 28 state layer, not this spec's ring and dot. The error condition (isError, the error forms of RAD-26 and RAD-27, given by a TanreiRadioGroup in error through its inherited scope) is not built on this platform yet. Controlled: onSelect(value) fires and the caller moves selected. React Native has no group idiom, so a RadioGroup parent (no tokens) carries the radiogroup role, its name and the arrow keys where a keyboard exists; it is the radio group component (radio-group), which adds the frame and holds the value. Lagging: it draws a 16 ring with a 1 stroke and an 8 dot in a 28 state layer, not this spec's ring and dot. The error condition (error, the error forms of RAD-26 and RAD-27, given by a RadioGroup in error through its context) is not built on this platform yet.

令牌

令牌 引用 解析值(浅色)
--tanrei-comp-radio-button-container-shape sys.shape.corner.full 9999px
--tanrei-comp-radio-button-container-size — 40px
--tanrei-comp-radio-button-disabled-container-shape — 9999px
--tanrei-comp-radio-button-disabled-opacity — 1
--tanrei-comp-radio-button-error-selected-container-color — #000000 · 0%
--tanrei-comp-radio-button-error-selected-disabled-container-color — #000000 · 0%
--tanrei-comp-radio-button-error-selected-disabled-fill-color — #1b1b1b · 38%
--tanrei-comp-radio-button-error-selected-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-radio-button-error-selected-disabled-ring-color — #1b1b1b · 38%
--tanrei-comp-radio-button-error-selected-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-radio-button-error-selected-fill-color comp.radio-button.error-selected.ring.color #cd262f
--tanrei-comp-radio-button-error-selected-focused-container-color — #cd262f · 10%
--tanrei-comp-radio-button-error-selected-focused-fill-color — #cd262f
--tanrei-comp-radio-button-error-selected-focused-label-color — #1b1b1b
--tanrei-comp-radio-button-error-selected-focused-ring-color — #cd262f
--tanrei-comp-radio-button-error-selected-focused-state-layer-color — #cd262f
--tanrei-comp-radio-button-error-selected-hovered-container-color — #cd262f · 8%
--tanrei-comp-radio-button-error-selected-hovered-fill-color — #cd262f
--tanrei-comp-radio-button-error-selected-hovered-label-color — #1b1b1b
--tanrei-comp-radio-button-error-selected-hovered-ring-color — #cd262f
--tanrei-comp-radio-button-error-selected-hovered-state-layer-color — #cd262f
--tanrei-comp-radio-button-error-selected-label-color sys.color.on-surface #1b1b1b
--tanrei-comp-radio-button-error-selected-pressed-container-color — #cd262f · 10%
--tanrei-comp-radio-button-error-selected-pressed-fill-color — #cd262f
--tanrei-comp-radio-button-error-selected-pressed-label-color — #1b1b1b
--tanrei-comp-radio-button-error-selected-pressed-ring-color — #cd262f
--tanrei-comp-radio-button-error-selected-pressed-state-layer-color — #cd262f
--tanrei-comp-radio-button-error-selected-ring-color sys.color.error #cd262f
--tanrei-comp-radio-button-error-selected-state-layer-color comp.radio-button.error-selected.ring.color #cd262f
--tanrei-comp-radio-button-error-unselected-container-color — #000000 · 0%
--tanrei-comp-radio-button-error-unselected-disabled-container-color — #000000 · 0%
--tanrei-comp-radio-button-error-unselected-disabled-fill-color — #000000 · 0%
--tanrei-comp-radio-button-error-unselected-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-radio-button-error-unselected-disabled-ring-color — #1b1b1b · 38%
--tanrei-comp-radio-button-error-unselected-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-radio-button-error-unselected-fill-color — #000000 · 0%
--tanrei-comp-radio-button-error-unselected-focused-container-color — #cd262f · 10%
--tanrei-comp-radio-button-error-unselected-focused-fill-color — #000000 · 0%
--tanrei-comp-radio-button-error-unselected-focused-label-color — #1b1b1b
--tanrei-comp-radio-button-error-unselected-focused-ring-color — #cd262f
--tanrei-comp-radio-button-error-unselected-focused-state-layer-color — #cd262f
--tanrei-comp-radio-button-error-unselected-hovered-container-color — #cd262f · 8%
--tanrei-comp-radio-button-error-unselected-hovered-fill-color — #000000 · 0%
--tanrei-comp-radio-button-error-unselected-hovered-label-color — #1b1b1b
--tanrei-comp-radio-button-error-unselected-hovered-ring-color — #cd262f
--tanrei-comp-radio-button-error-unselected-hovered-state-layer-color — #cd262f
--tanrei-comp-radio-button-error-unselected-label-color sys.color.on-surface #1b1b1b
--tanrei-comp-radio-button-error-unselected-pressed-container-color — #cd262f · 10%
--tanrei-comp-radio-button-error-unselected-pressed-fill-color — #000000 · 0%
--tanrei-comp-radio-button-error-unselected-pressed-label-color — #1b1b1b
--tanrei-comp-radio-button-error-unselected-pressed-ring-color — #cd262f
--tanrei-comp-radio-button-error-unselected-pressed-state-layer-color — #cd262f
--tanrei-comp-radio-button-error-unselected-ring-color sys.color.error #cd262f
--tanrei-comp-radio-button-error-unselected-state-layer-color comp.radio-button.error-unselected.ring.color #cd262f
--tanrei-comp-radio-button-fill-inset — 0px
--tanrei-comp-radio-button-fill-opacity sys.state.selected-state-layer-opacity 0.16
--tanrei-comp-radio-button-focused-container-shape — 9999px
--tanrei-comp-radio-button-focused-opacity — 1
--tanrei-comp-radio-button-hovered-container-shape — 9999px
--tanrei-comp-radio-button-hovered-opacity — 1
--tanrei-comp-radio-button-label-gap sys.space.space50 4px
--tanrei-comp-radio-button-motion-feedback-duration sys.motion.role.feedback 450ms
--tanrei-comp-radio-button-motion-selection-duration sys.motion.role.selection.mark 250ms
--tanrei-comp-radio-button-motion-selection-easing sys.motion.easing.state cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-radio-button-motion-state-duration sys.motion.role.state.compact 200ms
--tanrei-comp-radio-button-motion-state-easing sys.motion.easing.state cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-radio-button-option-min-height sys.target.touch 48px
--tanrei-comp-radio-button-pressed-container-shape — 9999px
--tanrei-comp-radio-button-pressed-opacity — 1
--tanrei-comp-radio-button-ring-selected-width-share — 0.5
--tanrei-comp-radio-button-ring-size — 20px
--tanrei-comp-radio-button-ring-width sys.border.thick 2px
--tanrei-comp-radio-button-selected-container-color — #000000 · 0%
--tanrei-comp-radio-button-selected-disabled-container-color — #000000 · 0%
--tanrei-comp-radio-button-selected-disabled-fill-color — #1b1b1b · 38%
--tanrei-comp-radio-button-selected-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-radio-button-selected-disabled-ring-color — #1b1b1b · 38%
--tanrei-comp-radio-button-selected-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-radio-button-selected-fill-color comp.radio-button.selected.ring.color #1b1b1b
--tanrei-comp-radio-button-selected-focused-container-color — #1b1b1b · 10%
--tanrei-comp-radio-button-selected-focused-fill-color — #1b1b1b
--tanrei-comp-radio-button-selected-focused-label-color — #1b1b1b
--tanrei-comp-radio-button-selected-focused-ring-color — #1b1b1b
--tanrei-comp-radio-button-selected-focused-state-layer-color — #1b1b1b
--tanrei-comp-radio-button-selected-hovered-container-color — #1b1b1b · 8%
--tanrei-comp-radio-button-selected-hovered-fill-color — #1b1b1b
--tanrei-comp-radio-button-selected-hovered-label-color — #1b1b1b
--tanrei-comp-radio-button-selected-hovered-ring-color — #1b1b1b
--tanrei-comp-radio-button-selected-hovered-state-layer-color — #1b1b1b
--tanrei-comp-radio-button-selected-label-color sys.color.on-surface #1b1b1b
--tanrei-comp-radio-button-selected-pressed-container-color — #1b1b1b · 10%
--tanrei-comp-radio-button-selected-pressed-fill-color — #1b1b1b
--tanrei-comp-radio-button-selected-pressed-label-color — #1b1b1b
--tanrei-comp-radio-button-selected-pressed-ring-color — #1b1b1b
--tanrei-comp-radio-button-selected-pressed-state-layer-color — #1b1b1b
--tanrei-comp-radio-button-selected-ring-color sys.color.primary #1b1b1b
--tanrei-comp-radio-button-selected-state-layer-color comp.radio-button.selected.ring.color #1b1b1b
--tanrei-comp-radio-button-unselected-container-color — #000000 · 0%
--tanrei-comp-radio-button-unselected-disabled-container-color — #000000 · 0%
--tanrei-comp-radio-button-unselected-disabled-fill-color — #000000 · 0%
--tanrei-comp-radio-button-unselected-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-radio-button-unselected-disabled-ring-color — #1b1b1b · 38%
--tanrei-comp-radio-button-unselected-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-radio-button-unselected-fill-color — #000000 · 0%
--tanrei-comp-radio-button-unselected-focused-container-color — #848484 · 10%
--tanrei-comp-radio-button-unselected-focused-fill-color — #000000 · 0%
--tanrei-comp-radio-button-unselected-focused-label-color — #1b1b1b
--tanrei-comp-radio-button-unselected-focused-ring-color — #848484
--tanrei-comp-radio-button-unselected-focused-state-layer-color — #848484
--tanrei-comp-radio-button-unselected-hovered-container-color — #848484 · 8%
--tanrei-comp-radio-button-unselected-hovered-fill-color — #000000 · 0%
--tanrei-comp-radio-button-unselected-hovered-label-color — #1b1b1b
--tanrei-comp-radio-button-unselected-hovered-ring-color — #848484
--tanrei-comp-radio-button-unselected-hovered-state-layer-color — #848484
--tanrei-comp-radio-button-unselected-label-color sys.color.on-surface #1b1b1b
--tanrei-comp-radio-button-unselected-pressed-container-color — #848484 · 10%
--tanrei-comp-radio-button-unselected-pressed-fill-color — #000000 · 0%
--tanrei-comp-radio-button-unselected-pressed-label-color — #1b1b1b
--tanrei-comp-radio-button-unselected-pressed-ring-color — #848484
--tanrei-comp-radio-button-unselected-pressed-state-layer-color — #848484
--tanrei-comp-radio-button-unselected-ring-color sys.color.outline #848484
--tanrei-comp-radio-button-unselected-state-layer-color comp.radio-button.unselected.ring.color #848484
--tanrei-comp-radio-button-label-typescale sys.typescale.body 400 14px/1.5714

测试样例

作为基准的渲染结果。所有平台都以相同的 ID 为每个样例截取快照,以便相互比较。

生成自 specs/radio-button.spec.json, tokens/comp/radio-button.tokens.json, design/tokens/

指南

何时使用

当一个问题恰好只有一个答案,并且只有少量需要用户并排比较的选项时,使用单选按钮,例如配送速度,或者多久发送一次摘要。所有选项同时可见,所以这个组会占用空间。选项很多或空间不足时,请使用只显示已选项的列表或菜单。

如果某个选项是安全且很可能被选的答案,组可以一开始就选中它。需要用户认真作答时,让所有选项都保持未选中。一旦选中了某个选项,用户就无法让这个组回到没有选择的状态,因为单选按钮绝不会因为自己被激活而取消选中。如果“无”是一个有效的答案,就把它加为一个选项。

复选框、单选按钮还是开关

控件 选项 何时生效 用途
复选框 任意数量的独立选项,也可以一项都不选 用户确认时 表单中的选项、同意条款、选择行
单选按钮 从全部显示出来的少量选项中恰好选一个 通常在用户确认时 一个问题的一个答案
开关 一项设置,开或关 立即 一经更改就生效的设置

推荐

  • 给每个组起一个名称:也就是这些选项所回答的问题,以可见文本的形式显示在组的前面。
  • 选项纵向堆叠,并按对这个问题有意义的顺序排列,例如从小到大。每个选项已经预留了目标的高度,所以堆叠时不需要额外的间隙。
  • 标签保持简短,句式一致,便于比较。
  • 把同一组的选项放在同一个容器里。

避免

  • 不要单独使用一个单选按钮。单个“是或否”的选择,请使用复选框或开关。
  • 不要把单选按钮用于并不互斥的选项。请使用复选框。
  • 不要在选项被选中的那一刻就启动重大的操作,例如跳转到别处。方向键会选中它经过的每个选项,键盘用户在移向目标选项的途中就会触发这个操作。
  • 不要把一个组拆散到屏幕的不同位置。

无障碍

角色
radio
标注
label is each option's accessible name, visible or not (labelHidden). The group's name (the question the options answer) is the radio group's label. Where options are grouped without a radio group (RAD-25) the product gives it: on the web a container with role radiogroup and aria-labelledby; on the other platforms the group container's label.
最小目标
即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px

键盘操作

按键 动作
Tab / Shift+Tab Moves focus into the group (to the selected option, or the first enabled one when none is selected) and out of it.
Space Selects the focused option if it is not selected; nothing while it is read-only.
Down Arrow / Right Arrow Moves to the next enabled option and selects it, wrapping (Left Arrow in right-to-left); an option that is read-only is focused and not selected.
Up Arrow / Left Arrow Moves to the previous enabled option and selects it, wrapping (Right Arrow in right-to-left); an option that is read-only is focused and not selected.
Enter Nothing on the web (it submits the form); selects where the platform activates any focused control with Enter (Android, tvOS).

备注

  • Scope: this spec covers the option, and what holds among the options of any group however it is formed (RAD-3, RAD-4, RAD-5, RAD-16). The radio group component (`radio-group`) is the group in the usual case: it holds the value, names the set and draws its label, its description and its error. Grouping without it (RAD-25) is for options that one container cannot lay out — one in each row of a table or a list, one on each card — and draws nothing, so it has no tokens.
  • iOS has no radio role: an option is exposed as a button with the selected trait, and its position ("2 of 3") through its accessibility value.
  • The ring alone carries the selected condition at 3:1 against the surface (WCAG 1.4.11): the selected ring is sys.color.primary, which the build holds to 4.5:1 on every surface role, and its stroke is 2.5 times the unselected one, so the selection does not rest on color alone (WCAG 1.4.1). The translucent fill is not counted for that, but it is a part of the mark, and WCAG 1.4.11 measures a part against the colors next to it: where the fill meets the ring (fill.inset 0) it is held 3:1 against the ring, and where an inset leaves a gap (a dot) it is held 3:1 against the gap, which shows what the radio stands on. The build holds it as drawn — the fill at comp.radio-button.fill.opacity and the ring, each over the state layer of the state — at rest, hovered, focused and pressed (a disabled option is exempt, as WCAG exempts an inactive control), on every surface role, in every scheme; in the default system the fill is the ring's color at 16 %, 3.17:1 from the ring at the least (the focused error form on surface-container-highest in light). In an error form the ring is sys.color.error, which the build holds to 4.5:1 on every surface role as it holds the primary role, so both error forms keep 3:1 (WCAG 1.4.11) and the selection is still told by the stroke.
  • A group in error is said by the radio group — its error text and its invalid state (RGP-8) — and shown on every option of it: the ring, and the fill of the selected one, take the error color (RAD-26), as a checkbox's box does (CHK-4). The color is one more sign and never the only one (WCAG 1.4.1): the words and the state carry the error, and the option exposes no invalid state of its own, because the radio role has none. The option has an error condition so that one form marks its checkboxes and its radio buttons by one rule.
  • Read-only (RAD-28) is said by the group, never by the option: WAI-ARIA has aria-readonly on radiogroup and not on radio. A radio group that is read-only says it (RGP-20); options grouped by name alone are read-only as a set when each is, and the product says it on the container it gives them (on the web, aria-readonly on the element with role radiogroup). HTML's readonly attribute does not apply to a radio button, so the option cancels its activation itself.

生成自 specs/radio-button.spec.json, tokens/sys/measure.tokens.json

组件负责的部分

  • 每个选项暴露单选角色、是否选中、禁用状态,以及它在组中的位置和组的大小(“2 of 3”)。Apple 的平台没有单选角色,所以在那里选项是一个带有选中特征的按钮,它的位置则作为无障碍值给出。Flutter 的语义目前还无法表达位置。
  • 标签就是无障碍名称,无论它是画出来的,还是用 labelHidden 隐藏的。画出来的文本对辅助技术隐藏,所以名称只读一次。
  • 一个组只有一个 Tab 停靠点:选中的选项;没有选项被选中时,是第一个可用的选项。方向键移到下一个或上一个可用的选项并选中它,到两端时循环;在从右到左的布局中,左方向键和右方向键互换。
  • Space 选中获得焦点的选项。Enter 只在平台用 Enter 激活任何获得焦点的控件时(Android、tvOS)才选中它;在 Web 上,Enter 提交表单。
  • 设置 readOnly 时,或外围的单选组为只读时,选项仍可获得焦点,方向键也仍能到达它,但没有任何操作会选中它:按下、Space、点击标签、click(),或方向键移到它上面都不会。它也没有悬停和按下的反馈。radio 角色没有只读状态,所以由组来说明。
  • 选中一个选项会取消其他选项的选中。只有变为选中的选项报告这次变化,激活已经选中的选项则什么都不会发生。
  • 点击或轻点标签会选中该选项。标签是点击区域的一部分,点击区域满足最小目标。每个选项都在布局中预留目标的高度,所以堆叠的选项各自保有完整、独立的目标。
  • 选中与否只由圆环来表达:在默认系统中,选中的圆环相对于每一种表面都保持 3:1 的对比度,并且粗细是未选中圆环的两倍半。半透明的填充不计入其中,但它与所接触的圆环在每一种状态、每一种表面和每一种配色方案下都保持 3:1 的对比度(WCAG 1.4.11),因此选中选项的中心也看得出来。
  • 它只在键盘聚焦时显示焦点指示。
  • 在减弱动态效果时,新的状态立即完整显示。
  • 缺少标签会在调试构建中报告。

已知的平台差距:

  • SwiftUI、Compose、Flutter 和 React Native 仍然绘制上一版的单选按钮:带圆点,使用以前的尺寸,还没有把圆环和填充画成一个形状,也不在布局中预留目标的高度,所以不留间隙堆叠的选项会共用目标区域。
  • readOnly 目前只在 Web 上提供:SwiftUI、Compose、Flutter 和 React Native 还没有。
  • 在 m3-like 下,选中时的圆点以完整大小淡入,而 Material 是让它从中心放大出现(模仿差距 radio-dot-scale-in)。

需要你做的事

  • 用组所回答的问题为它命名。在 Web 上,用带有 role="radiogroup" 和 aria-labelledby 的元素包裹这些选项,或者用带 <legend> 的 <fieldset>。在 Flutter 中,在 TanreiRadioGroup 上设置 semanticLabel;在 React Native 中,在 RadioGroup 上设置 accessibilityLabel。
  • 按平台的方式构成组,这样按键和位置才能正常工作:在 Web 上使用同一个 name,在 Compose 中给父级加上 Modifier.selectableGroup(),在 Flutter 中使用 TanreiRadioGroup,在 React Native 中使用 RadioGroup。
  • 当你自己用 name 组合选项并让每一项都只读时,请在容器上说明:在 role="radiogroup" 旁加上 aria-readonly="true"。设置了 readOnly 的单选组会替你做到这一点。
  • 如果问题必须回答却没有回答,把消息显示在组上:处于错误状态的单选组会绘制它的错误文本,并让每个单选按钮都以错误形态绘制。没有用单选组来分组的选项,要在每个单选按钮上设置 error:圆环和圆点采用错误颜色,与复选框的方框一样。禁用的选项不显示错误颜色。
  • 处于错误状态的单选按钮不会暴露为无效,因为它的角色没有这种状态。这一点由组来说明:单选组是无效的,并以它的错误文本作为描述。自己为选项分组时,要在它们旁边用文字说明错误。
  • 如果你在自己的层里重新设定圆环和填充的样式,要让圆环足够醒目,单靠它就能看出选中与否。构建不会检查这一点。
  • 在 React Native 中,产品不是英文时,向 RadioGroup 传入 positionText。默认情况下,位置朗读为“2 of 3”。