4.1 关键概念
- variant(变体)
- 组件在同尺寸下的视觉风格分支,如 Button 的
default / destructive / outline / secondary / ghost / link。由 cva 声明。 - size(尺寸)
- 另一维度的变体,控制高度/内边距,如
default / sm / lg / icon。variant 与 size 正交组合。 - cva(class-variance-authority)
- 把 variant/size 映射到 Tailwind class 的声明式库,返回一个函数:传入变体名,产出合并好的类名字符串。
- cn()
clsx + tailwind-merge,合并"组件内置类"与"外部传入 className",并对冲突的 Tailwind 类去重。- asChild 模式
- 基于 Radix
Slot,让组件把样式"下发"给子元素而非自己渲染标签。<Button asChild><a>...</a></Button>让链接长得像按钮。 - 受控 vs 非受控
- 输入类组件既可非受控(
defaultValue)也可受控(value + onChange/onValueChange)。表单库场景通常受控。 - Label 关联
- Label 组件用
htmlFor关联输入id,点击 Label 聚焦输入,且被屏幕阅读器朗读,属可访问性刚需。
4.2 添加组件
$ npx shadcn@latest add button input label textarea \
select checkbox radio-group switch
4.3 Button:变体全家桶
import { Button } from "@/components/ui/button";
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
// 尺寸
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon"><Plus /></Button>
// asChild:让链接拥有按钮样式
<Button asChild>
<a href="/login">去登录</a>
</Button>
4.4 用 cn() 追加自定义样式
// 外部 className 会被 cn() 合并,且冲突项以外部为准
<Button className="w-full mt-4">全宽按钮</Button>
// 条件类名
import { cn } from "@/lib/utils";
<Button className={cn("transition", isLoading && "opacity-60 pointer-events-none")}>
{isLoading ? "提交中…" : "提交"}
</Button>
为什么需要 tailwind-merge
如果 Button 内置 px-4,你又传 px-8,普通字符串拼接会得到 "px-4 px-8"——CSS 里后者不一定赢。twMerge 智能识别同类冲突,只保留最后一个,结果确定为 px-8。
4.5 Input 与 Label
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
<div className="grid gap-1.5">
<Label htmlFor="email">邮箱</Label>
<Input id="email" type="email" placeholder="you@example.com" />
</div>
4.6 Textarea
import { Textarea } from "@/components/ui/textarea";
<div className="grid gap-1.5">
<Label htmlFor="bio">简介</Label>
<Textarea id="bio" placeholder="介绍一下你自己" rows={4} />
</div>
4.7 Select(基于 Radix)
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
<Select onValueChange={(v) => console.log(v)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="选择框架" />
</SelectTrigger>
<SelectContent>
<SelectItem value="next">Next.js</SelectItem>
<SelectItem value="vite">Vite</SelectItem>
<SelectItem value="remix">Remix</SelectItem>
</SelectContent>
</Select>
为什么不用原生 select
Radix 的 Select 用 div 重建下拉,可自由设计选项样式、支持图标/分组/滚动,同时保留键盘导航与 ARIA。原生 <select> 无法定制选项外观。
4.8 Checkbox / RadioGroup / Switch
import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch";
// 复选框
<div className="flex items-center gap-2">
<Checkbox id="terms" />
<Label htmlFor="terms">同意条款</Label>
</div>
// 单选组
<RadioGroup defaultValue="card">
<div className="flex items-center gap-2">
<RadioGroupItem value="card" id="r1" /><Label htmlFor="r1">信用卡</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="wechat" id="r2" /><Label htmlFor="r2">微信</Label>
</div>
</RadioGroup>
// 开关(受控)
<div className="flex items-center gap-2">
<Switch id="notify" checked={on} onCheckedChange={setOn} />
<Label htmlFor="notify">开启通知</Label>
</div>
4.9 自定义一个新变体
因为代码是你的,加变体只是改文件。给 Button 加一个 success:
// components/ui/button.tsx —— 在 variants.variant 里加一行
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
success: "bg-green-600 text-white hover:bg-green-600/90", // 新增
// ...其余不变
}
// 立即可用:<Button variant="success">保存成功</Button>
4.10 表单基础对照
| 组件 | 受控 prop | 回调 | 底座 |
|---|---|---|---|
| Input / Textarea | value | onChange | 原生 |
| Select | value | onValueChange | Radix Select |
| Checkbox | checked | onCheckedChange | Radix Checkbox |
| RadioGroup | value | onValueChange | Radix RadioGroup |
| Switch | checked | onCheckedChange | Radix Switch |
4.11 小结与下一步
- cva 声明 variant/size,cn() 合并类名并去重,是 shadcn 组件的通用骨架。
- 输入类组件多为受控,Radix 版 Select/Checkbox 等用
onValueChange/onCheckedChange。 - 加自定义变体只需改组件源文件——因为代码归你所有。
- 下一章把这些散件组织成完整表单:react-hook-form + Zod 校验体系。