Skip to main content
PolyUI/docs

Checkbox 复选框

允许用户从一组选项中选择一个或多个项目。支持选中、未选中与不确定三种状态,完全遵循 WAI-ARIA Checkbox 规范。

安装

bash
npx polyui add checkbox

基础

tsx
import { useId } from "react"
import { Checkbox } from "@polyui/react/checkbox"
import { Label } from "@polyui/react/label"
export function CheckboxBasic() {
  const id = useId()
  return (
    <div className="flex items-center gap-2">
      <Checkbox id={id} />
      <Label htmlFor={id}>Accept terms and conditions</Label>
    </div>
  )
}

半选状态

tsx
import { useId, useState } from "react"
import { Checkbox } from "@polyui/react/checkbox"
import { Label } from "@polyui/react/label"
import { cn } from "@polyui/core/lib/utils"
export function CheckboxIndeterminate() {
  const id = useId()
  // Base UI Checkbox doesn't natively support indeterminate via prop,
  // so we render a custom visual using CSS
  const [state, setState] = useState<"unchecked" | "indeterminate" | "checked">("indeterminate")

  const handleChange = (checked: boolean) => {
    setState(checked ? "checked" : "unchecked")
  }

  return (
    <div className="flex items-center gap-2">
      <div
        role="checkbox"
        aria-checked={state === "indeterminate" ? "mixed" : state === "checked"}
        tabIndex={0}
        onClick={() =>
          setState((s) => (s === "unchecked" ? "indeterminate" : s === "indeterminate" ? "checked" : "unchecked"))
        }
        onKeyDown={(e) => {
          if (e.key === " " || e.key === "Enter") {
            e.preventDefault()
            setState((s) => (s === "unchecked" ? "indeterminate" : s === "indeterminate" ? "checked" : "unchecked"))
          }
        }}
        className={cn(
          "peer relative flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-[4px] border border-input transition-colors outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
          (state === "checked" || state === "indeterminate") && "border-primary bg-primary text-primary-foreground"
        )}
      >
        {state === "indeterminate" && <span className="block h-px w-2.5 rounded-full bg-current" />}
        {state === "checked" && (
          <svg viewBox="0 0 14 14" className="size-3.5" fill="none" stroke="currentColor" strokeWidth={2}>
            <polyline points="2,7 5.5,10.5 12,3" />
          </svg>
        )}
      </div>
      <Label
        htmlFor={id}
        onClick={() =>
          setState((s) => (s === "unchecked" ? "indeterminate" : s === "indeterminate" ? "checked" : "unchecked"))
        }
      >
        Indeterminate checkbox
      </Label>
    </div>
  )
}

虚线

tsx
import { useId } from "react"
import { Checkbox } from "@polyui/react/checkbox"
import { Label } from "@polyui/react/label"
export function CheckboxDashed() {
  const id = useId()
  return (
    <div className="flex items-center gap-2">
      <Checkbox id={id} className="border-primary border-dashed" />
      <Label htmlFor={id}>Accept terms and conditions</Label>
    </div>
  )
}

待办事项

tsx
import { useId, useState } from "react"
import { Checkbox } from "@polyui/react/checkbox"
import { Label } from "@polyui/react/label"
import { cn } from "@polyui/core/lib/utils"
export function CheckboxTodo() {
  const id = useId()
  const [checked, setChecked] = useState(true)
  return (
    <div className="flex items-center gap-2">
      <Checkbox id={id} checked={checked} onCheckedChange={(c) => setChecked(c)} />
      <Label htmlFor={id} className={cn(checked && "line-through text-muted-foreground")}>
        Simple todo list item
      </Label>
    </div>
  )
}

尺寸

tsx
import { Checkbox } from "@polyui/react/checkbox"
export function CheckboxSizes() {
  return (
    <div className="flex items-center gap-3">
      <Checkbox defaultChecked aria-label="Size default" />
      <Checkbox className="size-5" defaultChecked aria-label="Size medium" />
      <Checkbox className="size-6" defaultChecked aria-label="Size large" />
    </div>
  )
}

徽章

tsx
import { useState } from "react"
import { Checkbox } from "@polyui/react/checkbox"
import { Badge } from "@polyui/react/badge"
export function CheckboxBadge() {
  const snacks = ["Burger", "Pizza", "Drinks"]
  const [selected, setSelected] = useState<string[]>(["Burger", "Pizza"])

  return (
    <div className="flex items-center gap-2">
      {snacks.map((label) => (
        <Badge key={label} variant="secondary" className="relative gap-2 rounded-sm px-3 py-1.5">
          <Checkbox
            id={`snack-${label}`}
            checked={selected.includes(label)}
            onCheckedChange={(checked) =>
              setSelected(checked ? [...selected, label] : selected.filter((item) => item !== label))
            }
            className="data-unchecked:hidden"
          />
          <label htmlFor={`snack-${label}`} className="cursor-pointer select-none after:absolute after:inset-0">
            {label}
          </label>
        </Badge>
      ))}
    </div>
  )
}

带描述

By clicking this checkbox, you agree to the terms and conditions.

tsx
import { useId } from "react"
import { Checkbox } from "@polyui/react/checkbox"
import { Label } from "@polyui/react/label"
export function CheckboxDescription() {
  const id = useId()
  return (
    <div className="flex items-start gap-2">
      <Checkbox id={id} defaultChecked className="mt-0.5" />
      <div className="grid gap-1.5">
        <Label htmlFor={id} className="leading-4">
          Accept terms and conditions
        </Label>
        <p className="text-muted-foreground text-xs">
          By clicking this checkbox, you agree to the terms and conditions.
        </p>
      </div>
    </div>
  )
}

水平分组

tsx
import { Checkbox } from "@polyui/react/checkbox"
import { Label } from "@polyui/react/label"
export function CheckboxHorizontalGroup() {
  const technologies = ["React", "Next.js", "Remix"]
  return (
    <div className="space-y-4">
      <Label className="font-semibold">Technologies</Label>
      <div className="flex flex-wrap items-center gap-x-4 gap-y-2">
        {technologies.map((label) => (
          <div key={label} className="flex items-center gap-2">
            <Checkbox id={`tech-${label}`} />
            <Label htmlFor={`tech-${label}`}>{label}</Label>
          </div>
        ))}
      </div>
    </div>
  )
}

垂直分组

tsx
import { Checkbox } from "@polyui/react/checkbox"
import { Label } from "@polyui/react/label"
import { AppleIcon, CherryIcon, GrapeIcon } from "lucide-react"
export function CheckboxVerticalGroup() {
  const fruits = [
    { label: "Apple", icon: AppleIcon },
    { label: "Cherry", icon: CherryIcon },
    { label: "Grape", icon: GrapeIcon },
  ]
  return (
    <div className="space-y-4">
      <Label className="font-semibold">Favorite Fruits</Label>
      <div className="flex flex-col gap-4">
        {fruits.map(({ label, icon: Icon }) => (
          <div key={label} className="flex items-center gap-2">
            <Checkbox id={`fruit-${label}`} />
            <Label htmlFor={`fruit-${label}`} className="flex items-center gap-1.5">
              <Icon className="size-4" aria-hidden="true" />
              {label}
            </Label>
          </div>
        ))}
      </div>
    </div>
  )
}

颜色

tsx
import { Checkbox } from "@polyui/react/checkbox"
export function CheckboxColors() {
  return (
    <div className="flex items-center gap-3">
      <Checkbox
        defaultChecked
        className="data-checked:bg-destructive! data-checked:border-destructive focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40"
        aria-label="Color destructive"
      />
      <Checkbox
        defaultChecked
        className="data-checked:border-sky-600 data-checked:bg-sky-600 focus-visible:ring-sky-600/20 dark:data-checked:border-sky-400 dark:data-checked:bg-sky-400 dark:focus-visible:ring-sky-400/40"
        aria-label="Color info"
      />
      <Checkbox
        defaultChecked
        className="data-checked:border-green-600 data-checked:bg-green-600 focus-visible:ring-green-600/20 dark:data-checked:border-green-400 dark:data-checked:bg-green-400 dark:focus-visible:ring-green-400/40"
        aria-label="Color success"
      />
    </div>
  )
}

自定义图标

tsx
import { useState } from "react"
import { HeartIcon, StarIcon, CircleIcon } from "lucide-react"
import { cn } from "@polyui/core/lib/utils"
export function CheckboxCustomIcons() {
  const [heart, setHeart] = useState(true)
  const [star, setStar] = useState(true)
  const [circle, setCircle] = useState(true)

  return (
    <div className="flex items-center gap-3">
      <button
        role="checkbox"
        aria-checked={heart}
        onClick={() => setHeart((v) => !v)}
        className="focus-visible:ring-ring/50 rounded-sm outline-none focus-visible:ring-3"
        aria-label="Heart"
      >
        <HeartIcon className={cn("size-6 stroke-1", heart && "fill-destructive stroke-destructive")} />
      </button>
      <button
        role="checkbox"
        aria-checked={star}
        onClick={() => setStar((v) => !v)}
        className="focus-visible:ring-ring/50 rounded-sm outline-none focus-visible:ring-3"
        aria-label="Star"
      >
        <StarIcon
          className={cn(
            "size-6 stroke-1",
            star && "fill-amber-500 stroke-amber-500 dark:fill-amber-400 dark:stroke-amber-400"
          )}
        />
      </button>
      <button
        role="checkbox"
        aria-checked={circle}
        onClick={() => setCircle((v) => !v)}
        className="focus-visible:ring-ring/50 rounded-sm outline-none focus-visible:ring-3"
        aria-label="Circle"
      >
        <CircleIcon
          className={cn(
            "size-6 stroke-1",
            circle && "fill-green-600 stroke-green-600 dark:fill-green-400 dark:stroke-green-400"
          )}
        />
      </button>
    </div>
  )
}

填充图标

tsx
import { useState } from "react"
import { CircleCheckIcon } from "lucide-react"
import { cn } from "@polyui/core/lib/utils"
export function CheckboxFilledIcons() {
  const items = [
    { color: "bg-destructive", ringColor: "focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40" },
    {
      color: "bg-sky-600 dark:bg-sky-400",
      ringColor: "focus-visible:ring-sky-600/20 dark:focus-visible:ring-sky-400/40",
    },
    {
      color: "bg-green-600 dark:bg-green-400",
      ringColor: "focus-visible:ring-green-600/20 dark:focus-visible:ring-green-400/40",
    },
  ]
  const [checked, setChecked] = useState([true, true, true])

  return (
    <div className="flex items-center gap-2">
      {items.map((item, i) => (
        <button
          key={i}
          role="checkbox"
          aria-checked={checked[i]}
          onClick={() => setChecked((prev) => prev.map((v, idx) => (idx === i ? !v : v)))}
          className={cn(
            "flex size-7 shrink-0 items-center justify-center rounded-full shadow-xs outline-none focus-visible:ring-3",
            item.color,
            item.ringColor,
            !checked[i] && "opacity-40"
          )}
          aria-label={`Color ${i + 1}`}
        >
          {checked[i] && <CircleCheckIcon className="size-5 fill-white stroke-current" />}
        </button>
      ))}
    </div>
  )
}

卡片

tsx
import { useState } from "react"
import { Checkbox } from "@polyui/react/checkbox"
import { cn } from "@polyui/core/lib/utils"
export function CheckboxCard() {
  const [autoStart, setAutoStart] = useState(true)
  const [autoUpdate, setAutoUpdate] = useState(false)

  return (
    <div className="space-y-2">
      <label
        className={cn(
          "hover:bg-accent/50 flex cursor-pointer items-start gap-2 rounded-lg border p-3 transition-colors",
          autoStart && "border-blue-600 bg-blue-50 dark:border-blue-900 dark:bg-blue-950"
        )}
      >
        <Checkbox
          checked={autoStart}
          onCheckedChange={(c) => setAutoStart(c)}
          className="data-checked:border-blue-600 data-checked:bg-blue-600 dark:data-checked:border-blue-700 dark:data-checked:bg-blue-700"
        />
        <div className="grid gap-1.5">
          <p className="text-sm font-medium leading-none">Auto Start</p>
          <p className="text-muted-foreground text-sm">Starting with your OS.</p>
        </div>
      </label>
      <label
        className={cn(
          "hover:bg-accent/50 flex cursor-pointer items-start gap-2 rounded-lg border p-3 transition-colors",
          autoUpdate && "border-blue-600 bg-blue-50 dark:border-blue-900 dark:bg-blue-950"
        )}
      >
        <Checkbox
          checked={autoUpdate}
          onCheckedChange={(c) => setAutoUpdate(c)}
          className="data-checked:border-blue-600 data-checked:bg-blue-600 dark:data-checked:border-blue-700 dark:data-checked:bg-blue-700"
        />
        <div className="grid gap-1.5">
          <p className="text-sm font-medium leading-none">Auto update</p>
          <p className="text-muted-foreground text-sm">Download and install new version</p>
        </div>
      </label>
    </div>
  )
}

列表组

tsx
import { Checkbox } from "@polyui/react/checkbox"
import { Label } from "@polyui/react/label"
import { CodeIcon, ChartPieIcon, PaletteIcon } from "lucide-react"
export function CheckboxListGroup() {
  const skills = [
    { label: "Web Development", icon: CodeIcon },
    { label: "Data Analysis", icon: ChartPieIcon },
    { label: "Graphic Design", icon: PaletteIcon },
  ]
  return (
    <ul className="flex w-full flex-col divide-y rounded-md border">
      {skills.map(({ label, icon: Icon }) => (
        <li key={label}>
          <Label
            htmlFor={`skill-${label}`}
            className="flex cursor-pointer items-center justify-between gap-2 px-5 py-3"
          >
            <span className="flex items-center gap-2">
              <Icon className="size-4" />
              {label}
            </span>
            <Checkbox id={`skill-${label}`} />
          </Label>
        </li>
      ))}
    </ul>
  )
}

树形

tsx
import { useState } from "react"
import { Checkbox } from "@polyui/react/checkbox"
import { Label } from "@polyui/react/label"
import { cn } from "@polyui/core/lib/utils"
export function CheckboxTree() {
  const items = ["Child 1", "Child 2", "Child 3"]
  const [selected, setSelected] = useState<string[]>(["Child 1", "Child 2"])

  const parentState: boolean | "mixed" =
    selected.length === 0 ? false : selected.length === items.length ? true : "mixed"

  const toggleParent = () => {
    setSelected(parentState === true ? [] : items)
  }

  const toggleChild = (item: string) => {
    setSelected((prev) => (prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]))
  }

  return (
    <div className="space-y-2">
      <div className="flex items-center gap-2">
        <div
          role="checkbox"
          aria-checked={parentState === "mixed" ? "mixed" : parentState}
          tabIndex={0}
          onClick={toggleParent}
          onKeyDown={(e) => {
            if (e.key === " " || e.key === "Enter") {
              e.preventDefault()
              toggleParent()
            }
          }}
          className={cn(
            "relative flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-[4px] border border-input transition-colors outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
            (parentState === true || parentState === "mixed") && "border-primary bg-primary text-primary-foreground"
          )}
        >
          {parentState === "mixed" && <span className="block h-px w-2.5 rounded-full bg-current" />}
          {parentState === true && (
            <svg viewBox="0 0 14 14" className="size-3.5" fill="none" stroke="currentColor" strokeWidth={2}>
              <polyline points="2,7 5.5,10.5 12,3" />
            </svg>
          )}
        </div>
        <Label onClick={toggleParent} className="cursor-pointer">
          Select all
        </Label>
      </div>
      <div className="ml-6 space-y-2">
        {items.map((item) => (
          <div key={item} className="flex items-center gap-2">
            <Checkbox id={`tree-${item}`} checked={selected.includes(item)} onCheckedChange={() => toggleChild(item)} />
            <Label htmlFor={`tree-${item}`}>{item}</Label>
          </div>
        ))}
      </div>
    </div>
  )
}

表单

By clicking this checkbox, you agree to the terms and conditions.

tsx
import { useId, useState } from "react"
import { Checkbox } from "@polyui/react/checkbox"
import { Label } from "@polyui/react/label"
import { Button } from "@polyui/react/button"
export function CheckboxForm() {
  const id = useId()
  const [checked, setChecked] = useState(true)
  return (
    <div className="flex items-start gap-2">
      <Checkbox id={id} checked={checked} onCheckedChange={(c) => setChecked(c)} className="mt-0.5" />
      <div className="grid gap-2">
        <Label htmlFor={id} className="leading-4">
          Accept terms and conditions
        </Label>
        <p className="text-muted-foreground text-xs">
          By clicking this checkbox, you agree to the terms and conditions.
        </p>
        <div className="flex flex-wrap gap-2">
          <Button variant="outline" size="sm" onClick={() => setChecked(false)}>
            Reset
          </Button>
          <Button size="sm">Submit</Button>
        </div>
      </div>
    </div>
  )
}

动画效果

tsx
import { useId } from "react"
import { Checkbox } from "@polyui/react/checkbox"
import { Label } from "@polyui/react/label"
export function CheckboxAnimated() {
  const id = useId()
  return (
    <div className="flex items-center gap-2">
      <Checkbox id={id} defaultChecked className="transition-all duration-200" />
      <Label htmlFor={id}>Animated checkbox</Label>
    </div>
  )
}

待办动画

tsx
import { useId, useState } from "react"
import { Checkbox } from "@polyui/react/checkbox"
import { Label } from "@polyui/react/label"
import { cn } from "@polyui/core/lib/utils"
export function CheckboxAnimatedTodo() {
  const id = useId()
  const [checked, setChecked] = useState(true)
  return (
    <div className="flex items-center gap-2">
      <Checkbox
        id={id}
        checked={checked}
        onCheckedChange={(c) => setChecked(c)}
        className="rounded-full data-checked:border-blue-500 data-checked:bg-blue-500 focus-visible:border-blue-500 focus-visible:ring-blue-500/20"
      />
      <Label
        htmlFor={id}
        className={cn("relative transition-all duration-500", checked && "text-muted-foreground line-through")}
      >
        Animated todo list item
      </Label>
    </div>
  )
}

彩纸效果

tsx
import { useId, useState } from "react"
import { Checkbox } from "@polyui/react/checkbox"
import { Label } from "@polyui/react/label"
export function CheckboxConfetti() {
  const id = useId()
  const [showConfetti, setShowConfetti] = useState(false)

  const handleChange = (checked: boolean) => {
    if (checked) {
      setShowConfetti(true)
      setTimeout(() => setShowConfetti(false), 800)
    }
  }

  const colors = ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF"]

  return (
    <div className="relative flex items-center gap-2">
      <Checkbox id={id} onCheckedChange={handleChange} />
      <Label htmlFor={id}>Check to see magic</Label>
      {showConfetti && (
        <div className="pointer-events-none absolute inset-0">
          {Array.from({ length: 12 }).map((_, i) => {
            const angle = (i / 12) * Math.PI * 2
            const distance = 30 + (i % 3) * 10
            const color = colors[i % colors.length]
            return (
              <span
                key={i}
                className="absolute size-1.5 rounded-full"
                style={{
                  backgroundColor: color,
                  left: "50%",
                  top: "50%",
                  transform: `translate(${Math.cos(angle) * distance}px, ${Math.sin(angle) * distance}px)`,
                  animation: "confetti-pop 0.6s ease-out forwards",
                  animationDelay: `${i * 40}ms`,
                  opacity: 0,
                }}
              />
            )
          })}
        </div>
      )}
      <style>{`
        @keyframes confetti-pop {
          0% { transform: translate(0, 0) scale(0); opacity: 0; }
          50% { opacity: 1; }
          100% { opacity: 0; }
        }
      `}</style>
    </div>
  )
}

属性

属性类型默认值说明
checkedbooleanfalse复选框的选中状态
onCheckedChange(checked: boolean) => void选中状态变化时的回调
disabledbooleanfalse是否禁用复选框
indeterminatebooleanfalse不确定状态,常用于全选场景中部分选中时