Radio(单选框)与 RadioGroup 用于选项不多、只能选一个的场景,例如配送方式或套餐。组只保存一个值,整组只有一个 Tab 停留点,方向键在选项间移动并直接选中。可多选时用复选框;选项很多、需要收起时用选择器。
组合
RadioGroup
├── Radio
│ └── RadioIndicator
└── Radio
└── RadioIndicator每个 Radio 都要有 value,并和它的文字一起包在 label 里。组的值就是被选中那一项的 value;RadioIndicator 只在所属项选中时显示。
用法
import { Field, FieldLabel } from "@ui-sdk/react/field";
import { Radio, RadioGroup, RadioIndicator } from "@ui-sdk/react/radio";
const speeds = [{ value: "standard", label: "标准" }, { value: "express", label: "加急" }, { value: "overnight", label: "次日达" }];
<Field>
<FieldLabel>配送方式</FieldLabel>
<RadioGroup defaultValue="standard">
{speeds.map((speed) => (
<label key={speed.value}>
<Radio value={speed.value}><RadioIndicator /></Radio>
{speed.label}
</label>
))}
</RadioGroup>
</Field>;外层字段的标签为整组命名;不用字段时给组传 aria-label 或 aria-labelledby。不传 defaultValue 时初始没有选中项,Tab 会落在第一个可用选项上。
说明文字与选项卡
某个选项需要额外解释时,把它单独放进一个 Field,用 FieldLabel 与 FieldDescription:内层字段把两者关联到这一项,外层字段仍为整组命名。
<Field>
<FieldLabel>邮件通知</FieldLabel>
<RadioGroup defaultValue="daily">
<Field>
<Radio value="daily"><RadioIndicator /></Radio>
<div>
<FieldLabel>每日汇总</FieldLabel>
<FieldDescription>每天早上一封邮件。</FieldDescription>
</div>
</Field>
</RadioGroup>
</Field>把整张卡的内容包进同一个 label,卡片任意位置都可点击;卡片自身的选中外观用 :has([data-checked]) 表达,例如 [data-slot="card"]:has([data-checked]) { border-color: var(--color-brand); background: var(--color-brand-wash); }。
需要把一组选择连同标题、说明一起描述时,用 FieldSet 与 FieldLegend,再用 aria-labelledby 指向 legend 的 id、aria-describedby 指向说明的 id。
指示器动画
RadioIndicator 出现的首帧带 data-starting-style,退场期间带 data-ending-style,并在退场动画结束前保持挂载,因此可以直接用过渡:
[data-slot="radio-indicator"] {
transition: opacity 0.15s, scale 0.15s;
&:is([data-starting-style], [data-ending-style]) {
opacity: 0;
scale: 0.5;
}
}keepMounted 让指示器在未选中时也留在页面里(带 data-unchecked),可以做悬停预览小圆点这类效果。参见样式与动画。
受控、控制器与循环
const [value, setValue] = React.useState<string | null>("standard");
<RadioGroup value={value} onValueChange={(next) => setValue(next)} loop={false}>
…
</RadioGroup>;value 传 null 表示都不选中。onValueChange(value, details) 的 details.reason 区分 item-press、list-navigation、reset、restore 与 imperative,details.cancel() 可拦截变更,参见状态与事件。
方向键默认首尾循环,loop={false} 让它停在两端。要从组件树外操作,用 createRadioGroupController() 创建控制器传给组,调用 getValue / setValue / focus;setValue 对禁用项与只读组无效,被取消时返回 false。参见控制器。
表单与校验
给组传 name 才会提交值,不传则不提交任何数据;form 可关联页面中其他表单的 id,表单重置回到 defaultValue。required 在未选中时阻止提交;放进带 FieldError 的字段里,提交失败会在组下方显示错误并把焦点移到组上。
<Field>
<FieldLabel>订单通知</FieldLabel>
<RadioGroup name="updates" required>
<label>
<Radio value="email"><RadioIndicator /></Radio>
仅邮件
</label>
<label>
<Radio value="sms"><RadioIndicator /></Radio>
仅短信
</label>
</RadioGroup>
<FieldError>请选择订单通知方式。</FieldError>
</Field>disabled 可以放在组上或单个 Radio 上:禁用项不可聚焦、不可选中,方向键会跳过它;禁用的组不提交值。readOnly 保留焦点移动与提交,但不改变选中项。外层 <fieldset disabled> 会向下继承。参见表单。
样式与状态
- 组
radio-group:data-disabled、data-readonly、data-required。 - 选项
radio:选中带data-checked,未选中带data-unchecked,另有data-disabled、data-readonly、data-required。 - 指示器
radio-indicator:与所属选项相同的状态属性,另加data-starting-style、data-ending-style。
[data-slot="radio"] {
inline-size: 1rem;
block-size: 1rem;
border: 1px solid var(--color-border);
border-radius: 50%;
}
[data-slot="radio"][data-checked] {
border-color: var(--color-brand);
}
[data-slot="radio"][data-disabled] {
opacity: 0.45;
}Radio 渲染的是带 radio 角色的按钮,另有一个隐藏的原生 radio 参与表单,所以圆点、边框都画在 radio 与 radio-indicator 上。
API
RadioGroup:
| 属性 | 类型 | 说明 |
|---|---|---|
value |
T | null |
受控值,null 表示都不选中。 |
defaultValue |
T | null |
非受控初始选中值,重置回到它。 |
onValueChange |
(value, details) => void |
选中值将要变化时触发,可取消。 |
name / form |
string |
提交字段名(不传则不提交)与关联表单 id。 |
required |
boolean |
未选中时阻止提交。 |
disabled / readOnly |
boolean |
禁用整组、只读整组。 |
loop |
boolean |
方向键是否首尾循环,默认开启。 |
controller |
object |
createRadioGroupController() 创建的控制器。 |
Radio:value(string,必填,选中时组变成这个值)、disabled(boolean,跳过该项)。RadioIndicator:keepMounted(boolean,未选中时是否保留在页面里)。
键盘与无障碍
遵循 WAI-ARIA radio group 模式,行为与共享 name 的原生 radio 一致。
| 按键 | 行为 |
|---|---|
Tab |
进入组,落在选中项或第一个可用项,整组只停一次。 |
| 方向键 | 移到上一个或下一个选项并选中它,跳过禁用项。 |
Space |
选中当前聚焦的选项。 |
Enter |
提交所在表单。 |
组的名称来自 FieldLabel、FieldSet 的 legend 或 aria-label / aria-labelledby;每个选项的名称来自包裹它的 label 或内层 FieldLabel。