文章目录
前言
cva 是class-variance-authority库的核心 API,它是一种用于动态生成多变体 CSS 类名的工具,专门为处理组件样式的复杂变体(Variant)组合而设计。
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
一、cva 的核心作用
cva 允许你通过结构化配置定义组件的多种样式变体(如不同颜色、尺寸),然后根据传入的 variant 和 size 等参数动态生成最终的 className 字符串。
二、代码逐层解析
cva 基础调用
const buttonVariants = cva(
"基础样式类", // 所有变体共享的公共样式
variants: { / 变体定义 / },
defaultVariants: { / 默认变体 / }
);
参数详解
基础样式(第一个参数):
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive"
这些样式会应用到所有按钮变体上(如布局、禁用状态、焦点效果等)。
变体配置(variants 对象):
variant:定义颜色/风格变体(如 default、destructive)。
variant: {
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90",
outline: "border bg-background shadow-xs hover:bg-accent",
// ...其他变体
size:定义尺寸变体(如 sm、lg)。
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
// ...其他尺寸
默认变体(defaultVariants):
当不传参数时使用的默认值:
defaultVariants: {
variant: "default", // 默认使用 default 颜色变体
size: "default" // 默认使用 default 尺寸
三、动态生成 className
通过 buttonVariants({ variant, size, className }) 动态计算:
// 示例1:生成默认按钮样式
buttonVariants({});
// 输出: “基础样式类 + default变体样式 + default尺寸样式”
// 示例2:生成 destructive 小号按钮
buttonVariants({ variant: "destructive", size: "sm" });
// 输出: “基础样式类 + destructive变体样式 + sm尺寸样式”
// 示例3:合并外部传入的 className
buttonVariants({ variant: "outline", className: "custom-class" });
// 输出: “基础样式类 + outline变体样式 + default尺寸样式 + custom-class”
四、为什么使用 cva?
传统 CSS 的问题
手动拼接类名易出错:{variant} {size} ${className}
难以维护多变体组合逻辑。
cva 的优势
类型安全:通过 VariantProps 自动推断 variant 和 size 的合法值。
结构化配置:清晰分离基础样式、变体和默认值。
原子化组合:避免样式冲突,支持 Tailwind CSS 等工具。
五、与其他工具对比
工具 | 特点 |
---|---|
cva | 专注变体组合,轻量级,适合组件库开发 |
Tailwind CSS | 提供原子类,但变体组合需手动处理 |
Styled Components | CSS-in-JS 方案,更适合动态样式(如基于 props 的样式) |
CSS Modules | 局部作用域,但变体逻辑仍需手动编写 |
六、完整工作流程
定义变体:用 cva 配置组件的所有可能样式组合。
组件封装:通过 Button 组件暴露变体参数(variant、size)。
动态渲染:根据传入参数生成最终 className,并合并外部类名。
类型提示:利用 VariantProps 自动生成 TypeScript 类型。
七、关键代码片段解析
asChild 模式
const Comp = asChild ? Slot : "button";
Slot(来自 @radix-ui/react-slot):允许将按钮的样式和属性传递给子组件,实现样式透传。
应用场景:当需要将 渲染为 或其他标签时:
<Button asChild variant="link">
<a href="/">链接样式按钮</a>
</Button>
cn 工具
className={cn(buttonVariants({ variant, size, className }))}
cn:通常是一个合并类名的工具(如 clsx 或 tailwind-merge),用于解决类名冲突。
总结
cva 是一种声明式变体样式管理方案,通过结构化配置和类型安全的方式,大幅简化了多变体组件的开发。它在设计系统、组件库中尤其有用,能够保持样式的一致性和可维护性。