Tabs
缝线上的 32 高页签;当前页签下贴一条倾斜的和纸胶带,全站唯一的「你在这里」记号。
第 12 集1080p已发布
第 13 集1080p压制中
第 14 集1080p未放送
tabs-demo.tsx
import { Badge, StatusBadge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
const EPISODES = [
{ episode: "第 12 集", status: <StatusBadge tone="ok">已发布</StatusBadge> },
{ episode: "第 13 集", status: <StatusBadge tone="run">压制中</StatusBadge> },
{ episode: "第 14 集", status: <StatusBadge tone="mute">未放送</StatusBadge> },
]
export default function TabsDemo() {
return (
<Tabs defaultValue="progress" className="w-full max-w-md">
<TabsList>
<TabsTrigger value="progress">发布进度</TabsTrigger>
<TabsTrigger value="releases">资源</TabsTrigger>
<TabsTrigger value="groups">字幕组</TabsTrigger>
</TabsList>
<TabsContent value="progress" className="flex flex-col">
{EPISODES.map((row) => (
<div key={row.episode} className="anibt-stitch-under flex items-center gap-3 py-2.5 last:bg-none">
<span className="anibt-num text-[14px] font-semibold">{row.episode}</span>
<Badge variant="res" className="ml-auto">
1080p
</Badge>
{row.status}
</div>
))}
</TabsContent>
<TabsContent value="releases" className="py-2.5 text-[13px] text-anibt-ink2">
共 <span className="anibt-num font-semibold text-foreground">38</span> 条资源,最近一条来自喵萌奶茶屋。
</TabsContent>
<TabsContent value="groups" className="py-2.5 text-[13px] text-anibt-ink2">
喵萌奶茶屋、桜都字幕组、LoliHouse 正在跟这部番。
</TabsContent>
</Tabs>
)
}安装
npx shadcn@latest add @anibt/tabs第一次用之前:在 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/tabs.tsx
"use client" import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { Tabs as TabsPrimitive } from "radix-ui" import { cn } from "@/lib/utils" /** * Tabs — the AniBT 「贴纸手账」 tab strip (Radix Tabs). * * 32px tabs (13/600 `--anibt-ink2`, the pink wash under the pointer) over a * stitched line; the current tab turns to ink and wears a strip of washi tape * under its label, tilted 1.5° — the site's one "you are here" mark. The * recipes are `anibt-tabs`, `anibt-tab` and `anibt-tab-on` from * `@anibt/theme`. A disabled tab keeps a readable muted ink. * * The strip scrolls sideways instead of wrapping: a tab is one line, and * eight of them on a 390px phone have to go somewhere. * * For a sunk track with a sticker thumb (a view switch, a filter) use * `SegmentedControl` or `ToggleGroup`: that is a choice, not navigation. * `TabsList`'s `variant` (`default` / `line`) exists only so code written for * shadcn's tabs keeps compiling; both draw the taped strip. */ function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) { return <TabsPrimitive.Root data-slot="tabs" className={cn("flex min-w-0 flex-col gap-3", className)} {...props} /> } const tabsListVariants = cva( "anibt-tabs w-full justify-start overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", { variants: { variant: { default: "", line: "", }, }, defaultVariants: { variant: "default", }, } ) function TabsList({ className, variant = "default", ...props }: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>) { return ( <TabsPrimitive.List data-slot="tabs-list" data-variant={variant} className={cn(tabsListVariants({ variant }), className)} {...props} /> ) } function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) { return ( <TabsPrimitive.Trigger data-slot="tabs-trigger" className={cn( "anibt-tab cursor-pointer justify-center outline-hidden data-[state=active]:anibt-tab-on", "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid", "disabled:pointer-events-none disabled:text-muted-foreground", "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[state=active]:[&_svg:not([class*='text-'])]:text-anibt-mark", className )} {...props} /> ) } function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) { return ( <TabsPrimitive.Content data-slot="tabs-content" className={cn( "min-w-0 flex-1 rounded-[10px] outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid", className )} {...props} /> ) } export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
用法
用法
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
<Tabs defaultValue="progress">
<TabsList>
<TabsTrigger value="progress">发布进度</TabsTrigger>
<TabsTrigger value="releases">资源</TabsTrigger>
</TabsList>
<TabsContent value="progress">…</TabsContent>
<TabsContent value="releases">…</TabsContent>
</Tabs>图标与计数
当前页签是墨色字,下面贴一条倾斜的和纸胶带;图标变粉。
tabs-icons.tsx
import { CalendarDaysIcon, ListVideoIcon, MessageSquareIcon, UsersIcon } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
export default function TabsIcons() {
return (
<Tabs defaultValue="releases" className="w-full max-w-lg">
<TabsList>
<TabsTrigger value="schedule">
<CalendarDaysIcon />
放送
</TabsTrigger>
<TabsTrigger value="releases">
<ListVideoIcon />
资源
<Badge variant="count-quiet">38</Badge>
</TabsTrigger>
<TabsTrigger value="groups">
<UsersIcon />
字幕组
</TabsTrigger>
<TabsTrigger value="comments" disabled>
<MessageSquareIcon />
讨论
</TabsTrigger>
</TabsList>
<TabsContent value="schedule" className="py-2 text-[13px] text-anibt-ink2">
每周六 23:30 放送。
</TabsContent>
<TabsContent value="releases" className="py-2 text-[13px] text-anibt-ink2">
当前页签是墨色字,下面贴一条倾斜的和纸胶带;图标变粉。
</TabsContent>
<TabsContent value="groups" className="py-2 text-[13px] text-anibt-ink2">
3 个字幕组在跟。
</TabsContent>
</Tabs>
)
}页签是 13/600 的 ink2,悬停是粉色淡底;当前页签换成墨色,标签下面贴一条 6px 的条纹胶带,倾斜 1.5°,切换时从左往右贴上去。图标是 16px,当前页签的图标变粉。可以放一个 count-quiet 计数。禁用的页签字变灰,不变淡。
放不下时
tabs-overflow.tsx
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
const DAYS = ["全部", "周一", "周二", "周三", "周四", "周五", "周六", "周日"]
export default function TabsOverflow() {
return (
<Tabs defaultValue="周三" className="w-full max-w-[18rem]">
<TabsList aria-label="番组表">
{DAYS.map((day) => (
<TabsTrigger key={day} value={day}>
{day}
</TabsTrigger>
))}
</TabsList>
</Tabs>
)
}页签永远一行:放不下就横向滚动(不显示滚动条),不换行。手机上一排八个番组表页签就是这样。
什么时候不用 Tabs
Tabs 是同一页面里几块内容之间的切换。视图切换和筛选(列表 / 海报墙、简中 / 繁中)是「选择」,用 Segmented Control 或 Toggle Group,它们是下沉轨道里的贴纸拇指,不贴胶带。
属性
Tabs
valuestring默认 —受控的当前页签
defaultValuestring默认 —非受控的初始页签
onValueChange(value: string) => void默认 —切换时调用
activationMode"automatic" | "manual"默认 "automatic"方向键移过去就切换,还是要按回车
TabsList
variant"default" | "line"默认 "default"只为兼容 shadcn 的写法;两者都是贴胶带的页签条
基于 Radix Tabs。另外导出 tabsListVariants。
