Switch
36×20 的糖果开关:关是下沉的轨道,开是糖果粉,16px 的白色旋钮弹簧般滑过去。
switch-demo.tsx
import { Field, FieldLabel } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"
export default function SwitchDemo() {
return (
<Field orientation="horizontal" className="w-fit">
<Switch id="switch-demo-auto" defaultChecked />
<FieldLabel htmlFor="switch-demo-auto">新剧集自动推送到下载器</FieldLabel>
</Field>
)
}安装
npx shadcn@latest add @anibt/switch第一次用之前:在 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 radix-ui - 3. 把下面的文件放进项目components/ui/switch.tsx
"use client" import * as React from "react" import { Switch as SwitchPrimitive } from "radix-ui" import { cn } from "@/lib/utils" /** * Switch — A's candy switch (AniBT 「贴纸手账」). * * A 36×20 track, sunk and ringed off, candy on, with a 16px white knob that * springs across. The track carries the state; focus is a 2px `--ring` * outline outside it. `size="sm"` is a 28×16 track with a 12px knob for a * dense row. Disabled keeps the position readable: the sunk track in both * positions and a grey knob — never an opacity wash. * * The paint is the `anibt-switch` / `anibt-switch-on` recipe spelled out in * classes, so the disabled compounds win by order. */ function Switch({ className, size = "default", ...props }: React.ComponentProps<typeof SwitchPrimitive.Root> & { size?: "sm" | "default" }) { return ( <SwitchPrimitive.Root data-slot="switch" data-size={size} className={cn( "group/switch peer relative inline-flex shrink-0 items-center rounded-full bg-muted p-0.5 shadow-[inset_0_0_0_1.5px_var(--input)] outline-hidden transition-[background-color,box-shadow] duration-200 after:absolute after:-inset-x-2 after:-inset-y-2", "data-[size=default]:h-5 data-[size=default]:w-9 data-[size=sm]:h-4 data-[size=sm]:w-7", "enabled:hover:data-[state=unchecked]: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)]", "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: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)]", className )} {...props} > <SwitchPrimitive.Thumb data-slot="switch-thumb" className={cn( "pointer-events-none block rounded-full bg-white shadow-(--anibt-shadow-sticker) transition-transform duration-[350ms] ease-anibt-spring", "group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3", "data-[state=unchecked]:translate-x-0 group-data-[size=default]/switch:data-[state=checked]:translate-x-4 group-data-[size=sm]/switch:data-[state=checked]:translate-x-3", "group-disabled/switch:bg-input group-disabled/switch:shadow-none" )} /> </SwitchPrimitive.Root> ) } export { Switch }
用法
用法
import { Field, FieldLabel } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"
<Field orientation="horizontal">
<Switch id="auto-push" />
<FieldLabel htmlFor="auto-push">新剧集自动推送到下载器</FieldLabel>
</Field>开关用于立即生效的设置;要点「保存」才生效的选项用勾选框。
状态
关开小号禁用 · 关禁用 · 开出错
switch-states.tsx
import { Switch } from "@/components/ui/switch"
export default function SwitchStates() {
return (
<div className="grid grid-cols-3 items-center gap-x-8 gap-y-4 text-[12px] text-muted-foreground">
<span className="flex flex-col items-center gap-2">
<Switch aria-label="关" />关
</span>
<span className="flex flex-col items-center gap-2">
<Switch aria-label="开" defaultChecked />开
</span>
<span className="flex flex-col items-center gap-2">
<Switch aria-label="小号" size="sm" defaultChecked />小号
</span>
<span className="flex flex-col items-center gap-2">
<Switch aria-label="禁用(关)" disabled />禁用 · 关
</span>
<span className="flex flex-col items-center gap-2">
<Switch aria-label="禁用(开)" disabled defaultChecked />禁用 · 开
</span>
<span className="flex flex-col items-center gap-2">
<Switch aria-label="出错" aria-invalid />出错
</span>
</div>
)
}- 关:
muted底的下沉轨道、1.5pxinput色内边;悬停边变粉。 - 开:糖果渐变加深一档的底边;白色旋钮带贴纸阴影,弹簧曲线滑过去。
size="sm":28×16 的轨道、12px 的旋钮,放进紧凑的行。- 焦点是 2px
ring色轮廓;出错是同样的红色轮廓。 - 禁用:开和关都是下沉的轨道,旋钮变灰,但位置仍然看得出开关。
设置列表
switch-settings.tsx
import { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldTitle } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"
const SETTINGS = [
{ id: "episodes", title: "订阅的番剧更新", description: "有新剧集时在站内提醒你。", on: true },
{ id: "groups", title: "关注的字幕组发布", description: "包括合集和修正版。", on: true },
{ id: "weekly", title: "每周番组表", description: "每周一早上 8 点推送本周放送。" },
]
export default function SwitchSettings() {
return (
<FieldGroup className="w-full max-w-md gap-2">
{SETTINGS.map((setting) => (
<FieldLabel key={setting.id} htmlFor={`switch-settings-${setting.id}`}>
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>{setting.title}</FieldTitle>
<FieldDescription>{setting.description}</FieldDescription>
</FieldContent>
<Switch id={`switch-settings-${setting.id}`} defaultChecked={setting.on} />
</Field>
</FieldLabel>
))}
</FieldGroup>
)
}每条设置是一张井:标题 13/600、说明 12px,开关在右端。写法是 FieldLabel 包住横排的 Field,整张卡都能点。开着的设置仍是纸面——粉色淡底只留给「选中」。
属性
Switch
size"default" | "sm"默认 "default"36×20 / 28×16
checkedboolean默认 —受控的开关状态
defaultCheckedboolean默认 —非受控的初始状态
onCheckedChange(checked: boolean) => void默认 —切换时调用
disabledboolean默认 —禁用:轨道下沉、旋钮变灰
基于 Radix Switch,渲染成 button role="switch"。
