Skip to content

一、CVA 是什么?一句话理解

CVA = 用「配置」描述 class 组合规则,用「参数」生成 className

它解决的问题只有一个:

组件样式的“条件组合爆炸”

二、没有 CVA 会发生什么?(痛点)

❌ 传统写法(不可维护)

tsx
<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"}
  `}
>

问题:

  • 条件分散
  • 无类型提示
  • 新增一个 variant → 重写一堆 if

三、CVA 的核心 API

ts
import { cva } from "class-variance-authority"

最小模型

ts
const buttonVariants = cva(
  "base-class",
  {
    variants: {
      variant: {
        primary: "...",
        outline: "...",
      },
      size: {
        sm: "...",
        lg: "...",
      },
    },
  }
)

使用:

ts
buttonVariants({ variant: "primary", size: "lg" })

四、完整 Button 示例(重点)

ts
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",
    },
  }
)

使用:

tsx
<Button variant="outline" size="lg" />

五、variants / defaultVariants 本质

variants 是什么?

ts
variants: {
  size: {
    sm: "h-9",
    lg: "h-11",
  }
}

👉 key 是参数,value 是 class

defaultVariants 是什么?

ts
defaultVariants: {
  size: "md"
}

👉 不传参数时的默认值

六、复合条件:compoundVariants(很重要)

多个 variant 同时满足 时追加 class

ts
compoundVariants: [
  {
    variant: "outline",
    size: "lg",
    class: "uppercase"
  }
]

相当于:

ts
if (variant === "outline" && size === "lg") {
  class += " uppercase"
}

七、布尔型 variant(常用)

ts
variants: {
  disabled: {
    true: "opacity-50 pointer-events-none",
    false: ""
  }
}

使用:

tsx
<Button disabled />

八、CVA + TypeScript(核心优势)

自动推导 Props 类型

ts
import { VariantProps } from "class-variance-authority"

type ButtonVariants = VariantProps<typeof buttonVariants>

interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    ButtonVariants {}

效果:

tsx
<Button variant="xxx" /> ❌ TS 报错
<Button size="lg" /> ✅

九、className 合并(必考)

tsx
<button
  className={cn(buttonVariants({ variant, size }), className)}
/>

用户可以:

tsx
<Button className="w-full" />

十、CVA 的设计思想(非常重要)

1️⃣ 配置驱动,而不是逻辑驱动

❌ if / else ✅ declarative config

2️⃣ 样式 = 设计系统的一部分

ts
variant="destructive"

不是:

ts
bg-red-500

3️⃣ 样式是“有限状态机”

  • variant
  • size
  • state

十一、什么时候该用 CVA?

✅ 用在:

  • Button
  • Badge
  • Alert
  • Input
  • Tabs Trigger

❌ 不用在:

  • 页面级布局
  • 强业务耦合组件
  • 一次性组件

十二、实战:自己写一个 Badge(一步步)

ts
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",
    },
  }
)
tsx
function Badge({ variant }: VariantProps<typeof badgeVariants>) {
  return <span className={badgeVariants({ variant })} />
}

十三、常见误区

  • ❌ variant 里写几十个 utility
  • ❌ 把业务状态(loading)当 variant
  • ❌ 在 UI 组件里加业务判断

十四、你算“会 CVA”了吗?

你能:

  • 不看文档写出 CVA 配置
  • 正确拆 variant / size / state
  • 用 compoundVariants
  • 用 TS 类型约束组件 API