Badge
20 高的事实标签、模切白边贴纸、计数胶囊,以及带点和字的状态。
1080p简中内封字幕MKV
✦ NEW23:3012同步中
badge-demo.tsx
import { Badge, StatusBadge } from "@/components/ui/badge"
export default function BadgeDemo() {
return (
<div className="flex flex-col items-start gap-3">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="res">1080p</Badge>
<Badge variant="lang" face="text">
简中
</Badge>
<Badge face="text">内封字幕</Badge>
<Badge>MKV</Badge>
</div>
<div className="flex flex-wrap items-center gap-3">
<Badge variant="candy" tilt={-3}>
✦ NEW
</Badge>
<Badge variant="water" tilt={2}>
23:30
</Badge>
<Badge variant="count">12</Badge>
<StatusBadge tone="run">同步中</StatusBadge>
</div>
</div>
)
}安装
npx shadcn@latest add @anibt/badge第一次用之前:在 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 class-variance-authority radix-ui - 3. 把下面的文件放进项目components/ui/badge.tsx
import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { Slot } from "radix-ui" import { cn } from "@/lib/utils" /** * Badge, StatusBadge and StatusDot — the AniBT 「贴纸手账」 label family. * * `Badge`: * - tags (20px, radius 6): `default` (neutral — a spec, a category), `res` * (blue: resolution, format, time — in the round face), `lang` (pink: * language, subtitles), and the status words `success` / `warning` / * `destructive` (mint / amber / red, no dot). * - stickers (die-cut white rim, tilted by `tilt`): `sticker`, and the filled * `candy` and `water`. A label stuck on — use sparingly. * - counts (20px pill): `count` (candy) and `count-quiet` (neutral). * - `secondary` and `outline` exist only so code written against shadcn's * badge keeps compiling; both are the neutral tag. * * `StatusBadge` is a state with its word and a 6px dot: ok (mint), run (blue, * the dot blinks), fail (red), warn (amber), mute (grey), on (pink). The word * is required — colour never speaks alone. * * `StatusDot` is the label-less 6px dot beside copy that already says the * state (a legend, a rail count). It is `aria-hidden`. */ const badgeVariants = cva( "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid [&>svg]:pointer-events-none [&>svg]:size-3", { variants: { variant: { default: "anibt-tag", secondary: "anibt-tag", outline: "anibt-tag", res: "anibt-tag anibt-tag-res", lang: "anibt-tag anibt-tag-lang", success: "anibt-tag anibt-tag-ok", warning: "anibt-tag anibt-tag-warn", destructive: "anibt-tag anibt-tag-danger", sticker: "anibt-sticker", candy: "anibt-sticker anibt-sticker-candy", water: "anibt-sticker anibt-sticker-water", count: "anibt-count", "count-quiet": "anibt-count anibt-count-quiet", }, /** Specs and figures take the round face; a word takes the body face. */ face: { spec: "anibt-num", text: "", }, size: { default: "", /** A 24px tag for a header row. */ lg: "h-6 px-2.5", }, }, defaultVariants: { variant: "default", face: "spec", size: "default", }, } ) type BadgeProps = React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & { asChild?: boolean /** Tilt in degrees for the sticker variants (A uses −4 … 4). */ tilt?: number } function Badge({ className, variant, face, size, asChild = false, tilt, style, ...props }: BadgeProps) { const Comp = asChild ? Slot.Root : "span" const tiltStyle = tilt === undefined ? style : ({ ...style, "--anibt-tilt": `${tilt}deg` } as React.CSSProperties) return ( <Comp data-slot="badge" data-variant={variant ?? "default"} className={cn(badgeVariants({ variant, face, size }), className)} style={tiltStyle} {...props} /> ) } const statusBadgeVariants = cva("anibt-status", { variants: { tone: { ok: "anibt-status-ok", run: "anibt-status-run", fail: "anibt-status-fail", warn: "anibt-status-warn", mute: "anibt-status-mute", on: "anibt-status-on", }, }, defaultVariants: { tone: "mute", }, }) type StatusBadgeProps = React.ComponentProps<"span"> & VariantProps<typeof statusBadgeVariants> & { /** The word that names the state. */ children: React.ReactNode } function StatusBadge({ className, tone, ...props }: StatusBadgeProps) { return ( <span data-slot="status-badge" data-tone={tone ?? "mute"} className={cn(statusBadgeVariants({ tone }), className)} {...props} /> ) } const statusDotVariants = cva("anibt-dot", { variants: { variant: { default: "", ok: "anibt-dot-ok", warn: "anibt-dot-warn", danger: "anibt-dot-danger", time: "anibt-dot-time", hollow: "anibt-dot-hollow", mute: "anibt-dot-mute", }, }, defaultVariants: { variant: "default", }, }) type StatusDotProps = Omit<React.ComponentProps<"span">, "children"> & VariantProps<typeof statusDotVariants> function StatusDot({ className, variant, ...props }: StatusDotProps) { return ( <span data-slot="status-dot" aria-hidden="true" className={cn(statusDotVariants({ variant }), className)} {...props} /> ) } export { Badge, badgeVariants, StatusBadge, statusBadgeVariants, StatusDot, statusDotVariants } export type { BadgeProps, StatusBadgeProps, StatusDotProps }
用法
用法
import { Badge, StatusBadge, StatusDot } from "@/components/ui/badge"
<Badge variant="res">1080p</Badge>
<Badge variant="candy" tilt={-3}>✦ NEW</Badge>
<StatusBadge tone="ok">已同步</StatusBadge>标签
HEVC-10bit2160p1080p简繁内封日语
完结过期失效S2 · 24
badge-tags.tsx
import { Badge } from "@/components/ui/badge"
export default function BadgeTags() {
return (
<div className="flex flex-col items-start gap-3">
<div className="flex flex-wrap items-center gap-2">
<Badge>HEVC-10bit</Badge>
<Badge variant="res">2160p</Badge>
<Badge variant="res">1080p</Badge>
<Badge variant="lang" face="text">
简繁内封
</Badge>
<Badge variant="lang" face="text">
日语
</Badge>
</div>
<div className="flex flex-wrap items-center gap-2">
<Badge variant="success" face="text">
完结
</Badge>
<Badge variant="warning" face="text">
过期
</Badge>
<Badge variant="destructive" face="text">
失效
</Badge>
<Badge size="lg">S2 · 24</Badge>
</div>
</div>
)
}标签说的是这条记录的事实:规格、分类、语言。
default:中性,下沉底加一圈细边。res:蓝色,用于分辨率、格式、时间,圆体。lang:粉色,用于语言、字幕。success/warning/destructive:薄荷 / 琥珀 / 红色的状态词,没有点。带点的状态用StatusBadge。
face 默认是 "spec":拉丁字母和数字用 Fredoka 圆体。标签写的是中文词时用 face="text"。size="lg" 是表头里用的 24 高。
贴纸
ANIME✦ 推荐周六 23:30TV · 2026 秋
badge-stickers.tsx
import { Badge } from "@/components/ui/badge"
export default function BadgeStickers() {
return (
<div className="flex flex-wrap items-center gap-4 py-1">
<Badge variant="sticker" tilt={-4}>
ANIME
</Badge>
<Badge variant="candy" tilt={3}>
✦ 推荐
</Badge>
<Badge variant="water" tilt={-2}>
周六 23:30
</Badge>
<Badge variant="sticker">TV · 2026 秋</Badge>
</div>
)
}贴纸是贴上去的标签:2px 模切白边、一点阴影,tilt 控制倾斜角度(A 常用 −4° 到 4°)。candy 和 water 是实心的糖果粉、水蓝。少用,一屏两三个就够。
计数
399+128
badge-counts.tsx
import { BellIcon } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
export default function BadgeCounts() {
return (
<div className="flex flex-wrap items-center gap-4">
<Badge variant="count">3</Badge>
<Badge variant="count">99+</Badge>
<Badge variant="count-quiet">128</Badge>
<span className="relative inline-flex">
<Button size="icon" variant="ghost" aria-label="通知,3 条未读">
<BellIcon />
</Button>
<Badge variant="count" aria-hidden="true" className="absolute -top-1.5 -right-1.5">
3
</Badge>
</span>
</div>
)
}count 是 20 高的糖果胶囊,带贴纸白边,适合贴在图标按钮角上;count-quiet 是中性的计数,用在侧栏这类安静的位置。
状态
已同步同步中同步失败排队中已关闭订阅中
做种 12今晚更新你的订阅未开始
badge-status.tsx
import { StatusBadge, StatusDot } from "@/components/ui/badge"
export default function BadgeStatus() {
return (
<div className="flex flex-col items-start gap-3">
<div className="flex flex-wrap items-center gap-2">
<StatusBadge tone="ok">已同步</StatusBadge>
<StatusBadge tone="run">同步中</StatusBadge>
<StatusBadge tone="fail">同步失败</StatusBadge>
<StatusBadge tone="warn">排队中</StatusBadge>
<StatusBadge tone="mute">已关闭</StatusBadge>
<StatusBadge tone="on">订阅中</StatusBadge>
</div>
<div className="flex flex-wrap items-center gap-4 text-[12px] text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
<StatusDot variant="ok" />
做种 12
</span>
<span className="inline-flex items-center gap-1.5">
<StatusDot variant="time" />
今晚更新
</span>
<span className="inline-flex items-center gap-1.5">
<StatusDot />
你的订阅
</span>
<span className="inline-flex items-center gap-1.5">
<StatusDot variant="hollow" />
未开始
</span>
</div>
</div>
)
}StatusBadge 是一个淡底里放 6px 的点和一个词:ok 完成(薄荷)、run 进行中(蓝,点会闪)、fail 失败(红)、warn 需要注意(琥珀)、mute 关闭(灰)、on 启用 / 订阅中(粉)。必须有字,颜色从不单独表达状态。
StatusDot 只画点,给已经写明状态的文字配一个记号(图例、侧栏计数)。它是 aria-hidden 的。
属性
Badge
variant"default" | "res" | "lang" | "success" | "warning" | "destructive" | "sticker" | "candy" | "water" | "count" | "count-quiet" | "secondary" | "outline"默认 "default"secondary、outline 等同 default,只为兼容 shadcn
face"spec" | "text"默认 "spec"spec 用圆体写规格和数字,text 用正文字体写词
size"default" | "lg"默认 "default"20 高 / 24 高
tiltnumber默认 —贴纸的倾斜角度(度),写进 --anibt-tilt
asChildboolean默认 false把样式交给唯一的子元素
StatusBadge
tone"ok" | "run" | "fail" | "warn" | "mute" | "on"默认 "mute"状态色
childrenReactNode默认 —必填:状态的文字
StatusDot
variant"default" | "ok" | "warn" | "danger" | "time" | "hollow" | "mute"默认 "default"default 是粉色笔画;hollow 是空心圈
