wui
组件

多选框 Checkbox

支持选中、未选中与半选(不确定)状态的交互式多选控件,内置平滑勾选微动效与完整的键盘无障碍能力。

第三方依赖 · radix-ui第三方依赖 · class-variance-authority第三方依赖 · motion

基础用法

最常见的多选列表用法。点击多选框或其绑定的文字标签即可切换选中状态:

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
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 的全部原生与无障碍属性,支持以下配置:

属性类型默认值说明
checkedboolean | "indeterminate"—受控模式下复选框的选中状态。传入 "indeterminate" 表示半选(不确定)状态。
defaultCheckedboolean | "indeterminate"false非受控模式下复选框的初始选中状态。
onCheckedChange(checked: boolean | "indeterminate") => void—复选框状态发生改变时触发的回调函数。
size"sm" | "default" | "lg""default"复选框的物理尺寸密度(sm: 16px, default: 20px, lg: 24px)。
disabledbooleanfalse是否禁用交互与焦点聚焦。
requiredbooleanfalse在表单中是否为必填字段。
namestring—原生表单提交时该复选框对应的字段名称。
valuestring"on"原生表单提交时处于选中状态所代表的值。
asChildbooleanfalse是否将属性与行为合并到唯一的子元素上渲染。
classNamestring—应用于外层按钮元素的额外 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多项非互斥选择、从属层级全选/半选、协议确认延迟生效(通常随表单提交保存)批量任务选择、订阅偏好设置、同意条款
RadioGroup2~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 设置。开启减弱动态效果时,图标立即呈现,避免引发眩晕。