组件
单选框组 Radio Group
用于在一组互斥选项中选取单一值的表单控件,提供流畅的视觉反馈与完整的键盘无障碍导航。
基础用法
最常见的单选框组用法。用户在互斥的选项中只能选择其中一项,点击选项或其文字区域即可选中:
Loading…
安装与引入
通过 CLI 自动添加组件,或手动复制源码至项目中:
pnpm dlx @wui-design/cli@latest add @wui/radio-group安装基础依赖
pnpm add radix-ui class-variance-authority clsx tailwind-merge复制组件源码到
components/ui/radio-group.tsx"use client"
import * as React from "react"
import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
import { cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
function RadioGroup({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-2.5", className)}
{...props}
/>
)
}
const radioGroupItemVariants = cva(
"peer inline-flex shrink-0 cursor-pointer items-center justify-center rounded-full border border-input bg-background shadow-xs outline-none transition-[border-color,box-shadow,scale] duration-200 ease-out hover:border-ring/70 active:scale-[0.9] focus-visible:ring-[3px] focus-visible:ring-ring/35 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 data-[state=checked]:border-primary motion-reduce:transition-none",
{
variants: {
size: {
sm: "size-4",
default: "size-5",
lg: "size-6",
},
},
defaultVariants: { size: "default" },
}
)
export interface RadioGroupItemProps
extends React.ComponentProps<typeof RadioGroupPrimitive.Item> {
/** Physical size of the radio control. @default "default" */
size?: "sm" | "default" | "lg"
}
/** One option inside a RadioGroup. The dot springs in when selected and shrinks away when deselected. */
function RadioGroupItem({ className, size = "default", ...props }: RadioGroupItemProps) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
data-size={size}
className={cn(radioGroupItemVariants({ size }), className)}
{...props}
>
<RadioGroupPrimitive.Indicator
forceMount
data-slot="radio-group-indicator"
className="size-1/2 scale-0 rounded-full bg-primary opacity-0 transition-[scale,opacity] duration-150 ease-in data-[state=checked]:scale-100 data-[state=checked]:opacity-100 data-[state=checked]:duration-300 data-[state=checked]:ease-[cubic-bezier(0.34,1.56,0.64,1)] motion-reduce:transition-none"
/>
</RadioGroupPrimitive.Item>
)
}
export { RadioGroup, RadioGroupItem, radioGroupItemVariants }
属性 Props
RadioGroup (容器)
RadioGroup 继承 Radix UI RadioGroup.Root 的全部属性:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| value | string | — | 受控模式下当前选中的单选项值。 |
| defaultValue | string | — | 非受控模式下初始选中的单选项值。 |
| onValueChange | (value: string) => void | — | 选中的单选项改变时触发的回调函数,返回选中的最新值。 |
| disabled | boolean | false | 是否禁用整组单选项交互。 |
| name | string | — | 在原生表单提交时,用于标识该单选框组的字段名称。 |
| required | boolean | false | 在表单中是否为必选字段。 |
| orientation | "horizontal" | "vertical" | "vertical" | 单选框组的排版方向。影响键盘箭头方向键的导航行为。 |
| loop | boolean | true | 使用键盘方向键导航到最后一个选项时,是否循环回第一个选项。 |
| className | string | — | 应用于外层容器的额外 CSS 类名。 |
RadioGroupItem (单选项)
RadioGroupItem 继承 Radix UI RadioGroup.Item 的全部属性:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| value | string | — | 该单选项对应的唯一值,必填。 |
| size | "sm" | "default" | "lg" | "default" | 单选按钮的物理尺寸(sm: 16px, default: 20px, lg: 24px)。 |
| disabled | boolean | false | 是否单独禁用该单选项。 |
| required | boolean | false | 该项是否为必选项。 |
| className | string | — | 应用于单选按钮元素的额外 CSS 类名。 |
事件 Events
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| onValueChange | (value: string) => void | — | 用户点击不同单选项或使用键盘方向键切换选项时触发,返回选中的 value 字符串。 |
| onFocus | (event: React.FocusEvent<HTMLButtonElement>) => void | — | 单选项获得焦点时触发。 |
| onBlur | (event: React.FocusEvent<HTMLButtonElement>) => void | — | 单选项失去焦点时触发。 |
| onKeyDown | (event: React.KeyboardEvent<HTMLButtonElement>) => void | — | 在单选项获得焦点时按下键盘按键触发。 |
使用场景与设计规范
RadioGroup 用于在 2 至 5 个互斥选项 中清晰呈现所有候选并强制用户仅能选择一项。
选项类组件对比选型
| 组件类型 | 选项数量 | 互斥性 | 视觉展开度 | 典型适用场景 |
|---|---|---|---|---|
| RadioGroup | 2 ~ 5 项 | 互斥单选 | 完全平铺展开 | 支付方式选择、发票类型、计费周期 |
| Select | 5 项以上 | 单选 / 多选 | 收起为下拉框 | 国家地区选择、省市区级联、超长分类列表 |
| Checkbox | 任意项 | 多选 / 不互斥 | 平铺或树状 | 兴趣标签多选、增值服务订购、同意协议 |
| Segmented / Tabs | 2 ~ 4 项 | 互斥单选 | 分段控制条 | 视图模式切换(网格/列表)、图表时间范围(天/周/月) |
设计最佳实践
- 默认选中项 (Default Value):在绝大多数单选场景中,推荐始终提供一个合理的默认选中项(如推荐方案),避免出现不确定的“未选”状态造成用户认知负担。
- 避免过多选项导致页面拥挤:当选项超过 5 个或选项文本较长时,应改用
Select下拉选择器以节省垂直空间。 - 垂直对齐优先:对于带有描述文本的选项,优先采用垂直排列;水平排列仅适合文案极短(2~4 个字)且选项数量在 3 个以内的轻量场景(如性别、优先级)。
- 可点击区域最大化:始终使用
<label htmlFor="...">将单选框与文字标题、描述文本包裹起来,确保用户点击整行任意位置均可选中。
场景示例
受控模式
在需要精确监听状态变化或与外部状态进行双向数据流绑定时,使用 value 和 onValueChange:
Loading…
const [value, setValue] = React.useState("monthly")
return (
<RadioGroup value={value} onValueChange={setValue}>
<label htmlFor="r1" className="flex items-center gap-2">
<RadioGroupItem value="monthly" id="r1" />
<span>按月结算</span>
</label>
<label htmlFor="r2" className="flex items-center gap-2">
<RadioGroupItem value="yearly" id="r2" />
<span>按年结算</span>
</label>
</RadioGroup>
)尺寸
组件提供 sm (16px)、default (20px) 和 lg (24px) 三种尺寸:
Loading…
sm:适合紧凑行内过滤条、密集表格内嵌操作。default:通用表单项、设置面板及标准卡片。lg:移动端触控场景、主要配置向导页。
带辅助描述文本
为每个选项提供清晰的主标题与次级辅助说明,帮助用户在复杂的业务选择中迅速做出判断:
Loading…
水平排列布局
对于简短的属性选择(如任务优先级、性别、状态过滤),设置 orientation="horizontal" 并配合 flex 布局水平平铺:
Loading…
禁用状态
支持整组禁用或单独禁用特定不可选的项目(如售罄、权限不足或未解锁的功能):
Loading…
交互式单选卡片组
在 SaaS 订阅计划、部署规格选择等核心业务中,将单选框与卡片容器深度结合,呈现价格、推荐标签与功能特性:
Loading…
选中描边使用 motion 的共享布局动画(layoutId)在卡片之间滑动;单选点本身会以带回弹的缩放出现,取消选中时收缩消失。
表单校验与提交
在团队邀请或角色分配表单中,配合 aria-invalid 与动态错误提示,实现严谨的表单提交流程:
Loading…
无障碍与交互 Accessibility
- WAI-ARIA 标准规范:
- 容器自动声明
role="radiogroup"。 - 每个选项声明
role="radio",并自动同步aria-checked="true" | "false"。 - 选项整体作为单个 Tab 停靠点(Tab Stop),符合标准 Radio Group 无障碍规范。
- 容器自动声明
- 键盘导航规则:
- Tab:将焦点移动到当前选中的单选项上(若未选择则聚焦到第一项)。再次按 Tab 离开单选框组。
- 方向键 ↑ / ←:将焦点与选中状态切换至上一个单选项。
- 方向键 ↓ / →:将焦点与选中状态切换至下一个单选项。
- Space:激活并选中当前处于焦点的单选项。
- 排版方向映射:
- 当
orientation="horizontal"时,左右方向键映射顺畅,屏幕阅读器会准确读出水平单选组的定位信息。
- 当
- 标签关联与描述:
- 务必为每个
RadioGroupItem指定唯一id,并通过<label htmlFor="...">进行绑定,以确保屏幕阅读器在聚焦时朗读对应标签文本。
- 务必为每个