Tailwind CSS 的工具优先方法如果组织不当会导致 JSX 臃肿。2026 年,生态系统已在几种模式上达成共识:用 class-variance-authority (cva) 管理变体,用 cn() 助手合并类名,用 @apply 处理真正重复的模式,以及用 Headless UI 处理可访问的交互式组件。
class-variance-authority (cva):类型安全的变体
cva 是在 Tailwind 中管理组件变体的标准方式。它为变体、默认变体和复合变体提供完整的 TypeScript 类型——所有这些都无需 CSS-in-JS。
// Using class-variance-authority (cva) for type-safe variants
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
// Base classes applied to every variant
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
// Usage:
// <Button variant="outline" size="sm">Click me</Button>
// <Button variant="destructive">Delete</Button>何时使用 @apply
@apply 将重复的 Tailwind 类组合提取为 CSS 类。谨慎使用——仅适用于在多个文件中重复 10 次以上的模式。大多数模式应放在 React 组件中。
/* globals.css — using @apply to extract reusable patterns */
@layer components {
/* Card component pattern */
.card {
@apply rounded-lg border bg-card text-card-foreground shadow-sm;
}
.card-header {
@apply flex flex-col space-y-1.5 p-6;
}
.card-title {
@apply text-2xl font-semibold leading-none tracking-tight;
}
.card-content {
@apply p-6 pt-0;
}
/* Form field pattern */
.form-label {
@apply text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70;
}
.form-input {
@apply flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm
ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium
placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2
focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
}
/* Badge pattern */
.badge {
@apply inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold
transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2;
}
}
/* Use sparingly — prefer utility classes in JSX for maintainability */cn() 工具:安全合并类名
clsx 处理条件类,tailwind-merge 解决 Tailwind 冲突(如 px-4 + px-6 → px-6)。它们共同构成了每个 shadcn/ui 组件使用的 cn() 工具。
// lib/utils.ts — the cn() helper (used everywhere in shadcn/ui)
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
// clsx: conditionally join class names
// twMerge: intelligently merge Tailwind classes (handles conflicts)
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Examples:
cn('px-4 py-2', 'px-6') // 'py-2 px-6' (px-4 overridden)
cn('text-red-500', isActive && 'text-blue-500') // conditional
cn({ 'opacity-50': disabled, 'cursor-not-allowed': disabled })Headless UI:可访问的交互式组件
Headless UI(由 Tailwind Labs 提供)提供完全无样式的可访问交互式组件。你提供 Tailwind 类;它处理 ARIA 属性、键盘导航和焦点管理。
// Using Headless UI (Tailwind Labs) for accessible components
import { Listbox, Transition } from '@headlessui/react';
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid';
import { Fragment, useState } from 'react';
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
];
export function Select() {
const [selected, setSelected] = useState(people[0]);
return (
<Listbox value={selected} onChange={setSelected}>
<Listbox.Button className="relative w-full cursor-default rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm">
<span className="block truncate">{selected.name}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</Listbox.Button>
<Transition as={Fragment} leave="transition ease-in duration-100" leaveFrom="opacity-100" leaveTo="opacity-0">
<Listbox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{people.map((person) => (
<Listbox.Option key={person.id} value={person} className={({ active }) => cn('relative cursor-default select-none py-2 pl-10 pr-4', active ? 'bg-amber-100 text-amber-900' : 'text-gray-900')}>
{({ selected }) => (
<>
<span className={cn('block truncate', selected ? 'font-medium' : 'font-normal')}>{person.name}</span>
{selected ? <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600"><CheckIcon className="h-5 w-5" /></span> : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</Listbox>
);
}Tailwind 配置:设计令牌和主题
使用 CSS 自定义属性(变量)扩展 Tailwind 可以实现无 JavaScript 的主题切换。这是 shadcn/ui 和 Radix UI 主题使用的模式。
// tailwind.config.ts — extend with design tokens
import type { Config } from 'tailwindcss';
const config: Config = {
darkMode: ['class'],
content: ['./src/**/*.{ts,tsx}'],
theme: {
extend: {
colors: {
// CSS variable-based colors for theming
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
},
},
},
plugins: [require('tailwindcss-animate')],
};
export default config;组件库方法对比
| Library | Approach | Code Ownership | Theming | Accessibility |
|---|---|---|---|---|
| shadcn/ui | Copy-paste + Radix + CVA | You own the code | CSS variables | Radix primitives |
| Chakra UI | Component library | npm dependency | Theme object | Built-in |
| MUI (Material) | Component library | npm dependency | Theme provider | Built-in |
| Mantine | Component library | npm dependency | CSS variables | Built-in |
| DaisyUI | Tailwind plugin | Config only | CSS variables | Manual |
最佳实践
- 对具有 2 个以上变体的任何组件使用 cva。它在编译时捕获拼写错误并自动生成 TypeScript 类型。
- 始终使用 cn()(twMerge + clsx)合并类名。永远不要直接连接字符串——Tailwind 冲突会破坏样式。
- 避免对组件使用 @apply。仅将其用于真正的全局模式,如 .prose 或第三方库覆盖。
- 通过 tailwind.config.ts 中 CSS 变量的设计令牌可以实现暗模式和主题,无需重复工具类。
- 对于可访问的下拉菜单、模态框、工具提示——使用 Headless UI 或 Radix UI 基元。自己实现容易出错。
常见问题
我应该使用 Tailwind 还是 styled-components 等 CSS-in-JS 库?
2026 年,Tailwind + CVA + shadcn/ui 是新 React 项目的主流模式。CSS-in-JS 在 React Server Components 中有性能开销。Tailwind 在构建时生成静态 CSS,零运行时开销,并原生支持 RSC。
shadcn/ui 是什么,我应该使用它吗?
shadcn/ui 是使用 Radix UI + Tailwind + CVA 构建的复制粘贴组件集合。与传统组件库不同,你拥有源代码——组件存在于你的仓库中。这意味着完全自定义,无需对抗库样式。强烈推荐用于新项目。
如何使用 Tailwind 处理暗模式?
在 tailwind.config.ts 中添加 darkMode: "class"。然后为工具类添加 dark: 前缀(如 dark:bg-gray-900)。通过在 html 元素上添加/移除 dark 类来切换。
如何防止 Tailwind 类在生产中被清除?
在 tailwind.config.ts 中配置 content 数组以包含所有使用 Tailwind 类的文件。永远不要动态构建类名(如 bg-${color}-500)——Tailwind 的清除无法分析插值字符串。
Tailwind CSS 适合设计系统吗?
是的,但需要纪律。使用映射到 Tailwind 颜色/间距的设计令牌(CSS 变量)。使用 cva 创建组件库。记录所有变体组合。Vercel、Linear 等公司在大规模使用基于 Tailwind 的设计系统。