shadcn/ui CVA 是什么
# 一、CVA 是什么?一句话理解
CVA = 用「配置」描述 class 组合规则,用「参数」生成 className
它解决的问题只有一个:
组件样式的“条件组合爆炸”
# 二、没有 CVA 会发生什么?(痛点)
# ❌ 传统写法(不可维护)
<button
className={`
px-4 py-2 rounded
${variant === "primary" && "bg-blue-600 text-white"}
${variant === "outline" && "border"}
${size === "lg" && "h-12 text-lg"}
${disabled && "opacity-50"}
`}
>
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
问题:
- 条件分散
- 无类型提示
- 新增一个 variant → 重写一堆 if
# 三、CVA 的核心 API
import { cva } from "class-variance-authority"
1
# 最小模型
const buttonVariants = cva(
"base-class",
{
variants: {
variant: {
primary: "...",
outline: "...",
},
size: {
sm: "...",
lg: "...",
},
},
}
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
使用:
buttonVariants({ variant: "primary", size: "lg" })
1
# 四、完整 Button 示例(重点)
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md font-medium",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground",
outline: "border border-input",
ghost: "hover:bg-accent",
},
size: {
sm: "h-9 px-3",
md: "h-10 px-4",
lg: "h-11 px-8",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
}
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
使用:
<Button variant="outline" size="lg" />
1
# 五、variants / defaultVariants 本质
# variants 是什么?
variants: {
size: {
sm: "h-9",
lg: "h-11",
}
}
1
2
3
4
5
6
2
3
4
5
6
👉 key 是参数,value 是 class
# defaultVariants 是什么?
defaultVariants: {
size: "md"
}
1
2
3
2
3
👉 不传参数时的默认值
# 六、复合条件:compoundVariants(很重要)
当 多个 variant 同时满足 时追加 class
compoundVariants: [
{
variant: "outline",
size: "lg",
class: "uppercase"
}
]
1
2
3
4
5
6
7
2
3
4
5
6
7
相当于:
if (variant === "outline" && size === "lg") {
class += " uppercase"
}
1
2
3
2
3
# 七、布尔型 variant(常用)
variants: {
disabled: {
true: "opacity-50 pointer-events-none",
false: ""
}
}
1
2
3
4
5
6
2
3
4
5
6
使用:
<Button disabled />
1
# 八、CVA + TypeScript(核心优势)
# 自动推导 Props 类型
import { VariantProps } from "class-variance-authority"
type ButtonVariants = VariantProps<typeof buttonVariants>
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
ButtonVariants {}
1
2
3
4
5
6
7
2
3
4
5
6
7
效果:
<Button variant="xxx" /> ❌ TS 报错
<Button size="lg" /> ✅
1
2
2
# 九、className 合并(必考)
<button
className={cn(buttonVariants({ variant, size }), className)}
/>
1
2
3
2
3
用户可以:
<Button className="w-full" />
1
# 十、CVA 的设计思想(非常重要)
# 1️⃣ 配置驱动,而不是逻辑驱动
❌ if / else ✅ declarative config
# 2️⃣ 样式 = 设计系统的一部分
variant="destructive"
1
不是:
bg-red-500
1
# 3️⃣ 样式是“有限状态机”
- variant
- size
- state
# 十一、什么时候该用 CVA?
✅ 用在:
- Button
- Badge
- Alert
- Input
- Tabs Trigger
❌ 不用在:
- 页面级布局
- 强业务耦合组件
- 一次性组件
# 十二、实战:自己写一个 Badge(一步步)
const badgeVariants = cva(
"inline-flex items-center rounded-full px-2 py-1 text-xs",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground",
success: "bg-green-500 text-white",
warning: "bg-yellow-500 text-black",
},
},
defaultVariants: {
variant: "default",
},
}
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Badge({ variant }: VariantProps<typeof badgeVariants>) {
return <span className={badgeVariants({ variant })} />
}
1
2
3
2
3
# 十三、常见误区
- ❌ variant 里写几十个 utility
- ❌ 把业务状态(loading)当 variant
- ❌ 在 UI 组件里加业务判断
# 十四、你算“会 CVA”了吗?
你能:
- 不看文档写出 CVA 配置
- 正确拆 variant / size / state
- 用 compoundVariants
- 用 TS 类型约束组件 API
上次更新: 2026/01/07, 09:20:46