wui
组件

多选框 Checkbox

支持选中、未选中与不确定状态的多选控件。

基础示例

Loading…
pnpm dlx wui@latest add @wui/checkbox
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,transform] duration-200 ease-out hover:border-ring/70 active:scale-[0.92] focus-visible:ring-[3px] focus-visible:ring-ring/35 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary",
  {
    variants: {
      size: {
        sm: "size-4 rounded-[4px]",
        default: "size-5",
        lg: "size-6 rounded-md",
      },
    },
    defaultVariants: { size: "default" },
  }
)

export interface CheckboxProps
  extends React.ComponentProps<typeof CheckboxPrimitive.Root> {
  /** Physical size of the checkbox. @default "default" */
  size?: "sm" | "default" | "lg"
}

/** An accessible checkbox with a smoothly drawn checkmark. */
function Checkbox({
  className,
  size = "default",
  checked,
  defaultChecked = false,
  onCheckedChange,
  ...props
}: CheckboxProps) {
  const reduceMotion = useReducedMotion()
  const [internalChecked, setInternalChecked] = React.useState(defaultChecked)
  const currentChecked = checked ?? internalChecked

  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} mode="wait">
          {currentChecked ? (
            <motion.svg
              key={String(currentChecked)}
              className="size-[78%]"
              viewBox="0 0 16 16"
              fill="none"
              initial={reduceMotion ? false : { opacity: 0, scale: 0.55 }}
              animate={{ opacity: 1, scale: 1 }}
              exit={reduceMotion ? undefined : { opacity: 0, scale: 0.7 }}
              transition={reduceMotion ? { duration: 0 } : { type: "spring", stiffness: 560, damping: 30, mass: 0.55 }}
              aria-hidden="true"
            >
              <motion.path
                d={currentChecked === "indeterminate" ? "M3.5 8h9" : "M3.25 8.25 6.5 11.25 12.75 4.75"}
                stroke="currentColor"
                strokeWidth="2.25"
                strokeLinecap="round"
                strokeLinejoin="round"
                initial={reduceMotion ? false : { pathLength: 0 }}
                animate={{ pathLength: 1 }}
                transition={reduceMotion ? { duration: 0 } : { duration: 0.24, ease: [0.22, 1, 0.36, 1] }}
              />
            </motion.svg>
          ) : null}
        </AnimatePresence>
      </CheckboxPrimitive.Indicator>
    </CheckboxPrimitive.Root>
  )
}

export { Checkbox, checkboxVariants }

组件作用

Checkbox 用于在一组互不排斥的选项中选择零项或多项,也适用于同意条款等需要明确提交的布尔选择。全局主色填充、紧凑圆角与路径绘制勾号提供清晰且丝滑的反馈。

组件属性

PropTypeDefaultDescription
size"sm" | "default" | "lg"defaultPhysical size of the checkbox.
disabledboolean
asChildboolean

事件

onCheckedChange(checked) 在状态变化后触发,值可能是 truefalse"indeterminate"。其余键盘、焦点与表单属性会透传到底层 Radix Root。

拓展使用

使用 checked="indeterminate" 表示一组子选项仅部分选中,横线和勾号都会使用路径动画进入;取消选择时则轻柔缩回。使用 size="sm | default | lg" 调整控件尺寸,文字标签应通过原生 label 与控件关联。