样式
使用 Tailwind 类进行条件化和可组合的样式。
现代组件库需要灵活的样式系统,能够在不牺牲开发者体验的情况下处理复杂需求。将 Tailwind CSS 与智能类合并相结合,已成为构建可定制组件的强大模式。
这种方法解决了提供合理默认值与允许完全自定义之间的根本矛盾 —— 这是多年来困扰组件库的挑战。
传统样式的问题
传统的 CSS 方法常常导致特异性竞赛、样式冲突和不可预测的覆盖。当你向已经具有 bg-red-500 的组件传入 className="bg-blue-500" 时,哪个会生效?
如果不正确处理,两者都会生效,结果取决于许多因素 —— CSS 源顺序、类的特异性、打包器的类合并算法等等。
智能合并类
tailwind-merge 库通过理解 Tailwind 的类结构并智能地解决冲突来解决这个问题。当两个类针对相同的 CSS 属性时,它只保留最后一个。
// Both bg-red-500 and bg-blue-500 apply - unpredictable result
<Button className="bg-blue-500" />
// Renders: className="bg-red-500 bg-blue-500"import { twMerge } from 'tailwind-merge';
// bg-blue-500 wins as it comes last
const className = twMerge('bg-red-500', 'bg-blue-500');
// Returns: "bg-blue-500"这适用于所有 Tailwind 工具类:
twMerge('px-4 py-2', 'px-8'); // Returns: "py-2 px-8"
twMerge('text-sm', 'text-lg'); // Returns: "text-lg"
twMerge('hover:bg-red-500', 'hover:bg-blue-500'); // Returns: "hover:bg-blue-500"该库也理解 Tailwind 的修饰符系统:
// Modifiers are handled correctly
twMerge('hover:bg-red-500 focus:bg-red-500', 'hover:bg-blue-500');
// Returns: "focus:bg-red-500 hover:bg-blue-500"条件类
经常需要根据 props 或状态有条件地应用类。clsx 库提供了一个简洁的 API 来实现这一点:
import clsx from 'clsx';
// Basic conditionals
clsx('base', isActive && 'active');
// Returns: "base active" (if isActive is true)
// Object syntax
clsx('base', {
'active': isActive,
'disabled': isDisabled,
});
// Arrays
clsx(['base', isLarge ? 'text-lg' : 'text-sm']);
// Mixed
clsx(
'base',
['array-item'],
{ 'object-conditional': true },
isActive && 'conditional'
);一个常见的模式是将一组默认类与传入的 props 和我们任何自定义逻辑合并:
const Component = ({ className, ...props }: ComponentProps) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div
className={cn(
"rounded-lg border bg-white shadow-sm",
isOpen && "bg-blue-500",
className
)}
{...props}
/>
);
};cn 工具函数
由 shadcn/ui 推广的 cn 函数结合了 clsx 和 tailwind-merge,既提供了条件逻辑也提供了智能合并功能:
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}其强大之处在于顺序 —— 基础样式优先,条件样式其次,用户覆盖最后。这确保了可预测的行为,同时保持完全的自定义能力。
Class Variance Authority (CVA)
对于具有许多变体的复杂组件,手动管理条件类会变得难以维护。Class Variance Authority (CVA) 提供了一个声明式 API 来定义组件变体。
例如,下面是 shadcn/ui 中 Button 组件的一个摘录:
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 hover:bg-primary/90",
destructive:
"bg-destructive text-white 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 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",
},
}
)最佳实践
1. 顺序很重要
始终按以下顺序应用类:
- 基础样式(始终应用)
- 变体样式(基于 props)
- 条件样式(基于状态)
- 用户覆盖(className prop)
className={cn(
'base-styles', // 1. Base
variant && variantStyles, // 2. Variants
isActive && 'active', // 3. Conditionals
className // 4. User overrides
)}2. 为你的变体编写文档
使用 TypeScript 和 JSDoc 来记录每个变体的作用:
type ButtonProps = {
/**
* The visual style of the button
* @default "primary"
*/
variant?: 'primary' | 'secondary' | 'destructive' | 'ghost';
/**
* The size of the button
* @default "md"
*/
size?: 'sm' | 'md' | 'lg';
};3. 提取重复模式
如果你发现自己反复编写相同的条件逻辑,就将其抽取出来:
export const focusRing = 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500';
export const disabled = 'disabled:pointer-events-none disabled:opacity-50';
// Use in components
className={cn(focusRing, disabled, className)}迁移指南
如果你正在从不同的样式方法迁移,下面是如何调整常见模式:
从 CSS Modules
import styles from './Button.module.css';
<button className={`${styles.button} ${styles[variant]} ${className}`} />import { cn } from '@/lib/utils';
<button className={cn(
'px-4 py-2 rounded-lg',
variant === 'primary' && 'bg-blue-500 text-white',
className
)} />从 styled-components
const Button = styled.button<{ $primary?: boolean }>`
padding: 8px 16px;
background: ${props => props.$primary ? 'blue' : 'gray'};
`;function Button({ primary, className, ...props }) {
return (
<button
className={cn(
'px-4 py-2',
primary ? 'bg-blue-500' : 'bg-gray-500',
className
)}
{...props}
/>
);
}性能注意事项
尽管 clsx 和 tailwind-merge 都经过高度优化,但请记住以下建议:
-
在组件外部定义变体 - CVA 变体应在组件外部定义,以避免在每次渲染时重新创建。
-
对复杂计算进行记忆化 - 如果你有开销较大的条件逻辑,考虑使用记忆化:
const className = useMemo(
() => cn(
baseStyles,
expensiveComputation(props),
className
),
[props, className]
);- 对于动态值使用 CSS 变量 - 不要动态生成类名,改用 CSS 变量:
// Good
<div
className="bg-[var(--color)]"
style={{ '--color': dynamicColor } as React.CSSProperties}
/>
// Avoid
<div className={`bg-[${dynamicColor}]`} />将 Tailwind CSS、智能类合并和变体 API 结合使用,为组件样式提供了坚实的基础。这种方法可从简单按钮扩展到复杂的设计系统,同时保持可预测性和良好的开发者体验。