Checkbox
18px 的糖果勾选框:选中是糖果粉加一个弹出来的白色对勾,部分选中是一道横线。
checkbox-demo.tsx
import { Checkbox } from "@/components/ui/checkbox"
import { Field, FieldLabel } from "@/components/ui/field"
const FILTERS = [
{ id: "chs", label: "只看简体中文", checked: true },
{ id: "batch", label: "隐藏合集" },
{ id: "finished", label: "包含已完结的番剧", checked: true },
{ id: "2160p", label: "只看 2160p(暂无片源)", disabled: true },
]
export default function CheckboxDemo() {
return (
<div className="flex flex-col gap-3">
{FILTERS.map((filter) => (
<Field key={filter.id} orientation="horizontal">
<Checkbox id={`checkbox-demo-${filter.id}`} defaultChecked={filter.checked} disabled={filter.disabled} />
<FieldLabel htmlFor={`checkbox-demo-${filter.id}`}>{filter.label}</FieldLabel>
</Field>
))}
</div>
)
}安装
npx shadcn@latest add @anibt/checkbox第一次用之前:在 components.json 里登记 @anibt,再单独运行一次 npx shadcn@latest add @anibt/theme(见快速开始)。只有单独安装主题时,CLI 才会把 shadcn 的标准颜色换成白樱 / 夜樱;随组件带进来的主题只补上 AniBT 自己的变量和配方。
手动安装
- 1. 先装主题组件只读
@anibt/theme的 token 和配方,没有主题就没有样子。npx shadcn@latest add @anibt/theme - 2. 安装依赖
npm install lucide-react radix-ui - 3. 把下面的文件放进项目components/ui/checkbox.tsx
"use client" import * as React from "react" import { CheckIcon, MinusIcon } from "lucide-react" import { Checkbox as CheckboxPrimitive } from "radix-ui" import { cn } from "@/lib/utils" /** * Checkbox — A's candy box (AniBT 「贴纸手账」). * * 18px on radius 6: paper inside a 1.5px `--input` ring when empty (pink * under the pointer); checked is candy with a deeper bottom edge and a white * tick that pops in; indeterminate is the same candy with a dash. A ticked * box always shows its tick — the state never rides on colour alone. * * Focus is a 2px `--ring` outline 2px outside, so it never replaces the * box's own ring; invalid is the same outline in red. Disabled is the sunk * well under the weak ink, checked or not — still readable, never an * opacity wash. The hit area reaches 8px past the box. * * The paint is the `anibt-check` / `anibt-check-on` recipe spelled out in * classes, so the disabled compounds below win by order, not by luck. */ function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) { return ( <CheckboxPrimitive.Root data-slot="checkbox" className={cn( "group/checkbox peer relative inline-flex size-[18px] shrink-0 items-center justify-center rounded-[6px] bg-card text-anibt-candy-ink shadow-[inset_0_0_0_1.5px_var(--input)] outline-hidden transition-[background-color,box-shadow] duration-150 after:absolute after:-inset-2", "enabled:hover:shadow-[inset_0_0_0_1.5px_var(--anibt-wash-line)]", "data-[state=checked]:bg-anibt-candy-edge data-[state=checked]:bg-(image:--anibt-candy) data-[state=checked]:shadow-[0_1px_0_var(--anibt-candy-edge)]", "data-[state=indeterminate]:bg-anibt-candy-edge data-[state=indeterminate]:bg-(image:--anibt-candy) data-[state=indeterminate]:shadow-[0_1px_0_var(--anibt-candy-edge)]", "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid", "aria-invalid:outline-2 aria-invalid:outline-offset-2 aria-invalid:outline-anibt-danger-fill aria-invalid:outline-solid", "disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground disabled:shadow-[inset_0_0_0_1.5px_var(--border)]", "disabled:data-[state=checked]:bg-anibt-deep2 disabled:data-[state=checked]:bg-none disabled:data-[state=checked]:shadow-[inset_0_0_0_1.5px_var(--border)]", "disabled:data-[state=indeterminate]:bg-anibt-deep2 disabled:data-[state=indeterminate]:bg-none disabled:data-[state=indeterminate]:shadow-[inset_0_0_0_1.5px_var(--border)]", className )} {...props} > <CheckboxPrimitive.Indicator data-slot="checkbox-indicator" className="grid place-content-center text-current transition-none" > <CheckIcon className="size-3 animate-anibt-pop group-data-[state=indeterminate]/checkbox:hidden" strokeWidth={3.5} aria-hidden="true" /> <MinusIcon className="hidden size-3 group-data-[state=indeterminate]/checkbox:block" strokeWidth={3.5} aria-hidden="true" /> </CheckboxPrimitive.Indicator> </CheckboxPrimitive.Root> ) } export { Checkbox }
用法
用法
import { Checkbox } from "@/components/ui/checkbox"
import { Field, FieldLabel } from "@/components/ui/field"
<Field orientation="horizontal">
<Checkbox id="only-chs" defaultChecked />
<FieldLabel htmlFor="only-chs">只看简体中文</FieldLabel>
</Field>勾选框旁的文字是选项,不是字段标签:13/400 墨色。用 Field orientation="horizontal" 会自动排成这样。
状态
未选已选部分出错禁用禁用已选
checkbox-states.tsx
import { Checkbox } from "@/components/ui/checkbox"
export default function CheckboxStates() {
return (
<div className="grid grid-cols-3 items-center gap-x-8 gap-y-4 text-[12px] text-muted-foreground sm:grid-cols-6">
<span className="flex flex-col items-center gap-2">
<Checkbox aria-label="未选" />
未选
</span>
<span className="flex flex-col items-center gap-2">
<Checkbox aria-label="已选" defaultChecked />
已选
</span>
<span className="flex flex-col items-center gap-2">
<Checkbox aria-label="部分" checked="indeterminate" />
部分
</span>
<span className="flex flex-col items-center gap-2">
<Checkbox aria-label="出错" aria-invalid />
出错
</span>
<span className="flex flex-col items-center gap-2">
<Checkbox aria-label="禁用" disabled />
禁用
</span>
<span className="flex flex-col items-center gap-2">
<Checkbox aria-label="禁用且已选" disabled defaultChecked />
禁用已选
</span>
</div>
)
}- 未选:纸面、1.5px
input色内边;悬停边变粉。 - 已选:糖果渐变、深一档的底边、白色对勾弹入。多选里选中的项一定露出对勾,不只换颜色。
- 部分:
checked="indeterminate",同样的糖果底,中间一道白色横线。 - 焦点:2px
ring色轮廓,外偏 2px;出错(aria-invalid)是同样的红色轮廓。 - 禁用:下沉的浅底、弱色的勾,选没选都读得出来。
- 点击区域比方块大一圈(每边 8px),手指也好点。
全选
2 / 4
checkbox-select-all.tsx
"use client"
import { useState } from "react"
import { Checkbox } from "@/components/ui/checkbox"
import { Field, FieldLabel } from "@/components/ui/field"
const EPISODES = ["第 09 集", "第 10 集", "第 11 集", "第 12 集"]
export default function CheckboxSelectAll() {
const [picked, setPicked] = useState<string[]>(["第 11 集", "第 12 集"])
const all = picked.length === EPISODES.length
const some = picked.length > 0 && !all
return (
<div className="anibt-well flex w-full max-w-xs flex-col">
<Field orientation="horizontal" className="anibt-stitch-under px-3 py-2.5">
<Checkbox
id="checkbox-select-all"
checked={all ? true : some ? "indeterminate" : false}
onCheckedChange={(checked) => setPicked(checked === true ? EPISODES : [])}
/>
<FieldLabel htmlFor="checkbox-select-all">全部剧集</FieldLabel>
<span className="anibt-num ml-auto text-[12px] text-muted-foreground">
{picked.length} / {EPISODES.length}
</span>
</Field>
<div className="flex flex-col gap-2.5 px-3 py-3">
{EPISODES.map((episode, index) => (
<Field key={episode} orientation="horizontal">
<Checkbox
id={`checkbox-select-all-${index}`}
checked={picked.includes(episode)}
onCheckedChange={(checked) =>
setPicked((current) =>
checked === true ? [...current, episode] : current.filter((item) => item !== episode)
)
}
/>
<FieldLabel htmlFor={`checkbox-select-all-${index}`} className="anibt-num">
{episode}
</FieldLabel>
</Field>
))}
</div>
</div>
)
}属性
Checkbox
checkedboolean | "indeterminate"默认 —受控的选中状态
defaultCheckedboolean | "indeterminate"默认 —非受控的初始状态
onCheckedChange(checked: boolean | "indeterminate") => void默认 —状态改变时调用
disabledboolean默认 —禁用:浅底弱色
aria-invalidboolean默认 —出错:红色轮廓
基于 Radix Checkbox,渲染成 button role="checkbox";放进 <form> 时 Radix 会补一个隐藏的原生 input,name / value / required 照常提交。
