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>
)
}属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
checked | boolean | false | 复选框的选中状态 |
onCheckedChange | (checked: boolean) => void | — | 选中状态变化时的回调 |
disabled | boolean | false | 是否禁用复选框 |
indeterminate | boolean | false | 不确定状态,常用于全选场景中部分选中时 |