方式
clsx 合并 className(类名),特别是处理动态、条件类的场景。主要负责 Tailwind 合并处理。
- 官网:CLSX
bun add tailwind-variants
tailwind-merge 会覆盖冲突的类,并保持其他所有内容不变。主要负责 Tailwind 冲突处理。
- 官网:Tailwind Merge
bun add tailwind-merge
tailwind-variants 变体
- 官网:CVA
bun add class-variance-authority
CVA (设计图) -> clsx (逻辑组装) -> tailwind-merge (质检修整)。
1. clsx:逻辑组装
优雅地处理“条件判断”。
在 React 中,我们经常需要根据状态(比如 isActive, isDisabled)来切换类名。如果你用原生 JS 写,会很难看:
❌ 没有 clsx (痛苦的原生写法):
// 这种写法既容易出错,又难以阅读(注意空格!)
const className = `btn ${isActive ? 'bg-blue-500' : ''} ${isDisabled ? 'opacity-50' : ''}`;2
✅ 使用 clsx (优雅):
clsx 允许你传入对象、数组或字符串,它会自动剔除 false, null, undefined 的值,并把剩下的拼接成字符串。
import { clsx } from 'clsx';
// 传入对象:key 是类名,value 是布尔条件
const className = clsx({
'btn': true,
'bg-blue-500': isActive, // 只有 isActive 为 true 时才加
'opacity-50': isDisabled, // 只有 isDisabled 为 true 时才加
'text-white': !isDisabled
});
// 结果: "btn bg-blue-500 text-white" (假设 isActive=true, isDisabled=false)2
3
4
5
6
7
8
9
10
2. tailwind-merge:冲突解决
作用: 确保 “最后写的样式” 一定生效。
Tailwind 有一个隐形坑:CSS 的优先级是由定义顺序决定的,而不是你写在 className 里的顺序。
❌ 问题场景 (样式不生效):
假设封装了一个 Button 组件,默认有 px-4 (左右内边距 1rem)。
现在你在使用时,想把它改小一点,传入了 px-2。
function Button({ className }) {
// 最终字符串: "px-4 bg-blue-500 px-2"
return <button className={`px-4 bg-blue-500 ${className}`} />
}
// 使用:
<Button className="px-2" />2
3
4
5
6
7
结果: 按钮可能还是 px-4 的大小!
原因: 因为在 Tailwind 的 CSS 文件里,px-4 如果定义在 px-2 后面,那无论你类名怎么写,CSS 都会采纳 px-4。
✅ 使用 tailwind-merge (智能覆盖):
它能理解 Tailwind 的语法。它知道 px-4 和 px-2 是冲突的(都控制 padding-left/right),于是它会保留最后出现的那个。
JavaScript
import { twMerge } from 'tailwind-merge';
// 它会分析并删掉被覆盖的 px-4
const finalClass = twMerge("px-4 bg-blue-500", "px-2");
// 结果: "bg-blue-500 px-2" (干净、正确)2
3
4
5
3. cva :变体
它的作用: 定义组件的多种形态(Primary, Secondary, Small, Large)。
这其实是 tailwind-variants 的“轻量版”前身。
用法: 它的 API 和 tv 非常像,但它不包含冲突合并功能(所以它必须配合 tailwind-merge 用)。
import { cva } from "class-variance-authority";
const buttonVariants = cva(
// 1. 基础样式
"font-medium rounded",
{
// 2. 变体定义
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground",
outline: "border border-input bg-background",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
},
},
// 3. 默认值
defaultVariants: {
variant: "default",
size: "default",
},
}
)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
4. 终极合体 cn
// lib/utils.ts
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
// 这是一个从左到右的管道:
// 1. clsx: 负责处理条件逻辑(把对象/数组变成字符串)
// 2. twMerge: 负责清洗字符串(去掉冲突的 Tailwind 类)
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}2
3
4
5
6
7
8
9
10
应用场景:
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
export function MyComponent({ className, isError }) {
return (
<button
className={cn(
// 1. 应用 CVA 定义的变体样式
buttonVariants({ variant: "outline", size: "sm" }),
// 2. 应用条件逻辑 (clsx 的工作)
isError && "border-red-500 text-red-500",
// 3. 允许外部传入 className 覆盖 (twMerge 的工作)
className
)}
>
Click me
</button>
)
}
// 或者这么写也行
export function MyComponent({ className, isError, variant, size }) {
return (
<button
className={cn(
buttonVariants({ variant, size, className }),
isError && "border-red-500 text-red-500",
)}
>
Click me
</button>
)
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
总结
- cva: 用来定义“这个组件有哪些样子”。
- clsx: 用来判断“什么时候该用这个样子”。
- tailwind-merge: 用来保证“外部传入的样式一定能覆盖内部样式”。
< ~/ > MyNote