组件
多选框 Checkbox
支持选中、未选中与半选(不确定)状态的交互式多选控件,内置平滑勾选微动效与完整的键盘无障碍能力。
基础用法
最常见的多选列表用法。点击多选框或其绑定的文字标签即可切换选中状态:
Loading…
安装与引入
通过 CLI 自动添加组件,或手动复制源码至项目中:
pnpm dlx @wui-design/cli@latest add @wui/checkbox安装基础依赖与动效库
pnpm add radix-ui motion class-variance-authority clsx tailwind-merge复制组件源码到
components/ui/checkbox.tsx"use client"
import * as React from "react"
import { Checkbox as CheckboxPrimitive } from "radix-ui"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
import { cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
const checkboxVariants = cva(
"peer inline-flex shrink-0 cursor-pointer items-center justify-center rounded-[5px] border border-input bg-background text-primary-foreground shadow-xs outline-none transition-[background-color,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 data-[state=checked]:bg-primary data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary motion-reduce:transition-none",
{
variants: {
size: {
sm: "size-4 rounded-[4px]",
default: "size-5",
lg: "size-6 rounded-md",
},
},
defaultVariants: { size: "default" },
}
)
// Both shapes share three points so the check can morph into the dash and back.
const CHECK_PATH = "M3.25 8.25 L6.5 11.25 L12.75 4.75"
const DASH_PATH = "M3.75 8 L8 8 L12.25 8"
export interface CheckboxProps
extends React.ComponentProps<typeof CheckboxPrimitive.Root> {
/** Physical size of the checkbox. @default "default" */
size?: "sm" | "default" | "lg"
}
/** An accessible checkbox whose checkmark draws in and morphs into the indeterminate dash. */
function Checkbox({
className,
size = "default",
checked,
defaultChecked = false,
onCheckedChange,
...props
}: CheckboxProps) {
const reduceMotion = useReducedMotion()
const [internalChecked, setInternalChecked] = React.useState(defaultChecked)
const currentChecked = checked ?? internalChecked
const indeterminate = currentChecked === "indeterminate"
function handleCheckedChange(next: boolean | "indeterminate") {
if (checked === undefined) setInternalChecked(next)
onCheckedChange?.(next)
}
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
data-size={size}
className={cn(checkboxVariants({ size }), className)}
checked={checked}
defaultChecked={defaultChecked}
onCheckedChange={handleCheckedChange}
{...props}
>
<CheckboxPrimitive.Indicator
forceMount
data-slot="checkbox-indicator"
className="flex size-full items-center justify-center"
>
<AnimatePresence initial={false}>
{currentChecked ? (
<motion.svg
key="mark"
className="size-[78%]"
viewBox="0 0 16 16"
fill="none"
initial={reduceMotion ? false : { opacity: 0, scale: 0.6 }}
animate={{ opacity: 1, scale: 1 }}
exit={
reduceMotion
? { opacity: 0, transition: { duration: 0 } }
: { opacity: 0, scale: 0.6, transition: { duration: 0.12, ease: "easeIn" } }
}
transition={
reduceMotion
? { duration: 0 }
: { type: "spring", stiffness: 560, damping: 30, mass: 0.55 }
}
aria-hidden="true"
>
<motion.path
stroke="currentColor"
strokeWidth="2.25"
strokeLinecap="round"
strokeLinejoin="round"
initial={reduceMotion ? false : { pathLength: 0, d: indeterminate ? DASH_PATH : CHECK_PATH }}
animate={{ pathLength: 1, d: indeterminate ? DASH_PATH : CHECK_PATH }}
transition={
reduceMotion
? { duration: 0 }
: {
pathLength: { duration: 0.24, delay: 0.04, ease: [0.22, 1, 0.36, 1] },
d: { duration: 0.22, ease: [0.22, 1, 0.36, 1] },
}
}
/>
</motion.svg>
) : null}
</AnimatePresence>
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox, checkboxVariants }
属性 Props
Checkbox 继承底层 Radix UI 的全部原生与无障碍属性,支持以下配置:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| checked | boolean | "indeterminate" | — | 受控模式下复选框的选中状态。传入 "indeterminate" 表示半选(不确定)状态。 |
| defaultChecked | boolean | "indeterminate" | false | 非受控模式下复选框的初始选中状态。 |
| onCheckedChange | (checked: boolean | "indeterminate") => void | — | 复选框状态发生改变时触发的回调函数。 |
| size | "sm" | "default" | "lg" | "default" | 复选框的物理尺寸密度(sm: 16px, default: 20px, lg: 24px)。 |
| disabled | boolean | false | 是否禁用交互与焦点聚焦。 |
| required | boolean | false | 在表单中是否为必填字段。 |
| name | string | — | 原生表单提交时该复选框对应的字段名称。 |
| value | string | "on" | 原生表单提交时处于选中状态所代表的值。 |
| asChild | boolean | false | 是否将属性与行为合并到唯一的子元素上渲染。 |
| className | string | — | 应用于外层按钮元素的额外 CSS 类名。 |
事件 Events
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| onCheckedChange | (checked: boolean | "indeterminate") => void | — | 用户通过点击、触摸或键盘按键切换复选框状态时触发,返回最新的布尔值或 "indeterminate"。 |
| onFocus | (event: React.FocusEvent<HTMLButtonElement>) => void | — | 复选框获得焦点时触发。 |
| onBlur | (event: React.FocusEvent<HTMLButtonElement>) => void | — | 复选框失去焦点时触发。 |
| onKeyDown | (event: React.KeyboardEvent<HTMLButtonElement>) => void | — | 在复选框获得焦点时按下键盘按键触发(如空格键切换选中)。 |
使用场景与设计规范
Checkbox 适用于在一组选项中选择零项、一项或多项,或用于需要用户明确确认的单个二元选择(如同意协议条款)。
组件选型对比
| 控制类型 | 适用场景 | 交互与生效模式 | 典型用例 |
|---|---|---|---|
| Checkbox | 多项非互斥选择、从属层级全选/半选、协议确认 | 延迟生效(通常随表单提交保存) | 批量任务选择、订阅偏好设置、同意条款 |
| RadioGroup | 2~5 个互斥选项中必须单选一项 | 延迟生效(切换选项即选中该项) | 支付方式、配送方式、发票类型选择 |
| Switch | 独立二元状态的开关切换 | 即时生效(切换后立即触发后台更新) | 开启深色模式、推送通知开关、飞行模式 |
设计最佳实践
- 始终绑定文本标签:不要单独放置没有文字说明的复选框。通过
<label htmlFor="...">显式关联复选框id,扩大可点击热区。 - 清晰的文案肯定语:复选框文案应使用肯定语气描述选中后的结果(如“接收新版本通知”),避免使用双重否定(如“不接收通知”)。
- 半选(Indeterminate)状态的规范使用:半选状态只能用在包含子选项的父级复选框中,表示“子项部分被选中”。它是一个由子项状态计算得出的派生状态,而不是用户可以直接循环选中的第三种独立持久状态。
- 校验与错误反馈:对于协议勾选等必选项,校验失败时应将
aria-invalid="true"传给组件,并在复选框下方清晰呈现错误提示文案。
场景示例
受控模式
在需要精确管理多选状态或与外部状态保持双向同步时,使用 checked 和 onCheckedChange:
Loading…
const [checked, setChecked] = React.useState<boolean | "indeterminate">(true)
return (
<Checkbox
checked={checked}
onCheckedChange={setChecked}
/>
)尺寸
组件提供 sm (16px)、default (20px) 和 lg (24px) 三种尺寸以适应不同密度的排版:
Loading…
sm:适合数据表格中的批量选择列、紧凑下拉菜单。default:通用表单项、设置面板及标准卡片。lg:移动端触控场景、突出的核心协议确认区域。
带标签与描述
将复选框与主标题和副文本描述组合,为用户提供清晰的决策上下文:
Loading…
半选与全选联动 (Indeterminate)
当管理具有父子层级的列表时,父级多选框会根据子项的完成情况自动在“未选”、“半选(不确定)”和“全选”之间切换。对勾与半选横线共享同一条路径,状态变化时会直接形变过渡,而不是先消失再出现:
Loading…
禁用状态
当用户无权限修改、配置项处于只读模式或依赖项未满足时,使用 disabled 禁用交互:
Loading…
多选卡片组
在增值服务选配、功能包订购等现代 SaaS 场景中,将复选框包裹在卡片内,提供更大的点击面积与丰富的内容排版:
Loading…
表单校验与错误状态
在用户注册或提交表单时,为多选框设置 aria-invalid 会切换为危险色边框;配合 FormField 与 FormMessage,错误提示会平滑展开与收起:
Loading…
无障碍与交互 Accessibility
- ARIA 角色与属性:
- 自动渲染
role="checkbox"。 - 根据状态自动维护
aria-checked="true" | "false" | "mixed"(半选状态对应"mixed")。 - 支持
aria-invalid="true"与aria-required="true"。
- 自动渲染
- 键盘导航:
- Tab:在表单各控件间移动焦点,聚焦时显示清晰的高对比度焦点环(Focus Ring)。
- Space:切换选中 / 未选中状态。
- 屏幕阅读器支持:
- 务必使用
<label htmlFor="id">或aria-labelledby关联标签文案,确保屏幕阅读器能准确朗读选项名称与当前状态。 - 辅助说明文字可通过
aria-describedby关联至提示元素 ID。
- 务必使用
- 动态效果降级:
- 对勾的路径绘制、勾选与半选之间的形变以及按压缩放都会适配系统的
prefers-reduced-motion设置。开启减弱动态效果时,图标立即呈现,避免引发眩晕。
- 对勾的路径绘制、勾选与半选之间的形变以及按压缩放都会适配系统的