Toggle Group
按下后保持的一组按钮:下沉轨道里的贴纸拇指,或一排 28 高的筛选签,多选时选中的签露出对勾。
已选 2 项:简中、内封
toggle-group-demo.tsx
"use client"
import { useState } from "react"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
const TAGS = ["简中", "繁中", "日文", "内封", "外挂", "合集", "HEVC"]
export default function ToggleGroupDemo() {
const [picked, setPicked] = useState<string[]>(["简中", "内封"])
return (
<div className="flex max-w-md flex-col items-start gap-3">
<ToggleGroup type="multiple" variant="outline" value={picked} onValueChange={setPicked} aria-label="筛选">
{TAGS.map((tag) => (
<ToggleGroupItem key={tag} value={tag}>
{tag}
</ToggleGroupItem>
))}
</ToggleGroup>
<p className="text-[12px] text-muted-foreground">
已选 <span className="anibt-num">{picked.length}</span> 项:{picked.join("、") || "无"}
</p>
</div>
)
}安装
npx shadcn@latest add @anibt/toggle-group第一次用之前:在 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 lucide-react radix-ui - 3. 把下面的文件放进项目components/ui/toggle-group.tsx
"use client" import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { CheckIcon } from "lucide-react" import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui" import { cn } from "@/lib/utils" /** * ToggleGroup — buttons that stay pressed (AniBT 「贴纸手账」, Radix * ToggleGroup): a view switch, a formatting bar, a row of filters. * * - `variant="default"`: A's segmented track — the sunk `--muted` well on * radius 10 with 4px of padding; items are 24px, 12/600 `--anibt-ink2`; * a pressed item is a white sticker thumb in ink with its glyph in pink. * - `variant="outline"`: A's filter chips — 28px paper keys under a 1px * `--input` ring that hop 1px under the pointer; a pressed chip is the * pink wash with a pink ring and pink ink. In a `type="multiple"` group * each pressed chip also shows a tick, so a multi-select never rides on * colour alone. * * `type="single"` can be pressed off again; when one choice must always * hold (a form value), use `SegmentedControl`. `size` is `sm` / `default` / * `lg`; `spacing` is the gap in Tailwind steps (2px track, 8px chips by * default). Sizes and variant names follow shadcn's toggle. */ const toggleGroupItemVariants = cva( "group/toggle-item inline-flex min-w-0 shrink-0 cursor-pointer items-center justify-center gap-1 text-[12px] leading-none font-semibold whitespace-nowrap outline-hidden focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring focus-visible:outline-solid disabled:pointer-events-none disabled:text-muted-foreground aria-invalid:outline-2 aria-invalid:outline-anibt-danger-fill aria-invalid:outline-solid [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { default: "rounded-[6px] bg-transparent text-anibt-ink2 transition-[color,background-color,box-shadow] duration-200 ease-anibt-out hover:text-foreground [&_svg]:text-muted-foreground data-[state=on]:bg-card data-[state=on]:text-foreground data-[state=on]:shadow-(--anibt-shadow-sticker) data-[state=on]:[&_svg]:text-anibt-mark", outline: "rounded-[10px] bg-card text-anibt-ink2 shadow-[inset_0_0_0_1px_var(--input)] transition-[transform,color,background-color,box-shadow] duration-200 ease-anibt-spring hover:-translate-y-px hover:text-accent-foreground data-[state=on]:bg-accent data-[state=on]:text-accent-foreground data-[state=on]:shadow-[inset_0_0_0_1px_var(--anibt-wash-line)] disabled:bg-muted disabled:shadow-[inset_0_0_0_1px_var(--border)]", }, size: { sm: "", default: "", lg: "", }, }, compoundVariants: [ { variant: "default", size: "sm", class: "h-6 min-w-6 px-1.5" }, { variant: "default", size: "default", class: "h-6 min-w-6 px-2" }, { variant: "default", size: "lg", class: "h-8 min-w-8 px-2.5 text-[13px]" }, { variant: "outline", size: "sm", class: "h-6 min-w-6 px-2.5" }, { variant: "outline", size: "default", class: "h-7 min-w-7 px-3" }, { variant: "outline", size: "lg", class: "h-8 min-w-8 px-3.5 text-[13px]" }, ], defaultVariants: { variant: "default", size: "default", }, } ) type ToggleGroupContextValue = VariantProps<typeof toggleGroupItemVariants> & { type: "single" | "multiple" } const ToggleGroupContext = React.createContext<ToggleGroupContextValue>({ variant: "default", size: "default", type: "single", }) function ToggleGroup({ className, variant = "default", size = "default", spacing, orientation = "horizontal", style, children, ...props }: React.ComponentProps<typeof ToggleGroupPrimitive.Root> & VariantProps<typeof toggleGroupItemVariants> & { /** The gap between items, in Tailwind spacing steps. */ spacing?: number }) { const gap = spacing ?? (variant === "outline" ? 2 : 0.5) const context = React.useMemo(() => ({ variant, size, type: props.type }), [variant, size, props.type]) return ( <ToggleGroupPrimitive.Root data-slot="toggle-group" data-variant={variant} data-size={size} data-spacing={gap} data-orientation={orientation} orientation={orientation} style={{ "--gap": gap, ...style } as React.CSSProperties} className={cn( "group/toggle-group flex w-fit max-w-full items-center gap-[--spacing(var(--gap))] data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch", variant === "outline" ? "flex-wrap" : "rounded-[10px] bg-muted p-1 shadow-[inset_0_0_0_1px_var(--border)]", className )} {...props} > <ToggleGroupContext.Provider value={context}>{children}</ToggleGroupContext.Provider> </ToggleGroupPrimitive.Root> ) } function ToggleGroupItem({ className, children, variant, size, ...props }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleGroupItemVariants>) { const context = React.useContext(ToggleGroupContext) const itemVariant = context.variant ?? variant const itemSize = context.size ?? size const ticks = itemVariant === "outline" && context.type === "multiple" return ( <ToggleGroupPrimitive.Item data-slot="toggle-group-item" data-variant={itemVariant} data-size={itemSize} className={cn(toggleGroupItemVariants({ variant: itemVariant, size: itemSize }), className)} {...props} > {ticks ? ( <CheckIcon aria-hidden="true" strokeWidth={3} className="-ml-0.5 hidden size-3.5 animate-anibt-pop group-data-[state=on]/toggle-item:block" /> ) : null} {children} </ToggleGroupPrimitive.Item> ) } export { ToggleGroup, ToggleGroupItem, toggleGroupItemVariants }
用法
用法
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
<ToggleGroup type="multiple" variant="outline" defaultValue={["简中"]} aria-label="筛选">
<ToggleGroupItem value="简中">简中</ToggleGroupItem>
<ToggleGroupItem value="繁中">繁中</ToggleGroupItem>
<ToggleGroupItem value="内封">内封</ToggleGroupItem>
</ToggleGroup>两种样子
toggle-group-track.tsx
import { BoldIcon, CodeIcon, ItalicIcon, LayoutGridIcon, ListIcon, StrikethroughIcon } from "lucide-react"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
export default function ToggleGroupTrack() {
return (
<div className="flex flex-wrap items-center gap-4">
<ToggleGroup type="single" defaultValue="grid" aria-label="视图">
<ToggleGroupItem value="list">
<ListIcon />
列表
</ToggleGroupItem>
<ToggleGroupItem value="grid">
<LayoutGridIcon />
海报墙
</ToggleGroupItem>
</ToggleGroup>
<ToggleGroup type="multiple" defaultValue={["bold"]} aria-label="文字格式">
<ToggleGroupItem value="bold" aria-label="加粗">
<BoldIcon />
</ToggleGroupItem>
<ToggleGroupItem value="italic" aria-label="斜体">
<ItalicIcon />
</ToggleGroupItem>
<ToggleGroupItem value="strike" aria-label="删除线">
<StrikethroughIcon />
</ToggleGroupItem>
<ToggleGroupItem value="code" aria-label="代码" disabled>
<CodeIcon />
</ToggleGroupItem>
</ToggleGroup>
</div>
)
}variant="default":分段轨道。muted底的下沉轨道,项 24 高、12/600ink2;按下的一项是白色贴纸拇指、墨色字、粉色图标。适合视图切换和格式工具栏。variant="outline":筛选签。28 高的纸面小键加一圈input色细边,悬停上跳 1px、字变粉;按下的签是粉色淡底、粉边、粉字。type="multiple"时按下的签前面多一个对勾,多选从不只靠颜色。
type="single" 的项按第二下会弹起;必须一直选着一项(表单值)时用 Segmented Control。
尺寸
toggle-group-sizes.tsx
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
const DAYS = ["一", "二", "三", "四", "五", "六", "日"]
export default function ToggleGroupSizes() {
return (
<div className="flex flex-col items-start gap-4">
{(["sm", "default", "lg"] as const).map((size) => (
<div key={size} className="flex flex-wrap items-center gap-4">
<ToggleGroup type="single" size={size} defaultValue="三" aria-label={`星期(${size})`}>
{DAYS.map((day) => (
<ToggleGroupItem key={day} value={day}>
{day}
</ToggleGroupItem>
))}
</ToggleGroup>
<ToggleGroup type="single" variant="outline" size={size} defaultValue="tv" aria-label={`类型(${size})`}>
<ToggleGroupItem value="tv">TV</ToggleGroupItem>
<ToggleGroupItem value="movie">剧场版</ToggleGroupItem>
<ToggleGroupItem value="ova">OVA</ToggleGroupItem>
</ToggleGroup>
</div>
))}
</div>
)
}sm / default / lg:轨道里是 24 / 24 / 32 高,筛选签是 24 / 28 / 32 高。spacing 是项之间的间距(Tailwind 的步长),默认轨道 2px、筛选签 8px;筛选签放不下时自动换行。orientation="vertical" 竖排。
属性
ToggleGroup
type"single" | "multiple"默认 —单选(可弹起)或多选;必填
variant"default" | "outline"默认 "default"分段轨道 / 筛选签
size"sm" | "default" | "lg"默认 "default"见上文
spacingnumber默认 0.5 / 2项之间的间距,Tailwind 步长
orientation"horizontal" | "vertical"默认 "horizontal"排列方向,也决定方向键
value / defaultValuestring | string[]默认 —受控 / 非受控的值;multiple 时是数组
onValueChange(value) => void默认 —按下或弹起时调用
基于 Radix ToggleGroup:每项是 aria-pressed 的按钮,方向键在项之间移动。另外导出 toggleGroupItemVariants。
