1. CSS in React 的三大挑战
全局命名污染
普通 CSS 的选择器都是全局的。在 ComponentA.css 中定义 .title { color: red; },会影响整个应用中所有带 class="title" 的元素,不论它们在哪个组件中。
命名冲突
随着项目规模增大,开发者需要记住所有已使用的类名,避免重名。BEM(.block__element--modifier)等命名规范试图解决这个问题,但依赖人工约束,难以维护。
CSS 与 JS 状态同步
组件有很多动态样式需求(展开/收起、主题切换、错误状态),需要在 CSS 和 JavaScript 之间传递状态,用字符串拼接类名既繁琐又容易出错。
2. 样式方案全景对比
| 方案 | 隔离性 | 动态样式 | 学习成本 | 包体积 | 推荐场景 |
|---|---|---|---|---|---|
| 原生 CSS | 无(全局) | 需手动拼类名 | 最低 | 最小 | 小项目、快速原型 |
| CSS Modules | 文件级别 | 需手动拼类名 | 低 | 小 | 传统组件库、精细控制 |
| Styled-components | 组件级别 | 原生 JS 表达式 | 中 | 较大(运行时) | 设计系统、主题化 |
| Tailwind CSS | 无(但不需要) | 条件类名 | 中(需记类名) | 极小(purge) | 快速开发、新项目首选 |
| Sass/SCSS | 无(需手动) | 需手动 | 低 | 小 | 老项目迁移、嵌套样式 |
3. CSS Modules 详解
CSS Modules 是一种让 CSS 文件中的类名在构建时自动转换为唯一哈希值的技术。每个组件的样式天然隔离,不需要担心命名冲突。Vite 内置支持,文件名以 .module.css 结尾即可。
/* Button.module.css — 写法与普通 CSS 完全一样 */
/* .button 在构建后会变成类似 _button_3kx2a_1 的唯一类名 */
.button {
padding: 8px 16px;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.primary {
background: #3178C6;
color: white;
}
.danger {
background: #ef4444;
color: white;
}
.disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* :global — 该选择器不做哈希转换,保持全局 */
:global(.third-party-widget) {
border: 1px solid red;
}
// Button.tsx — 导入 CSS Modules
import styles from './Button.module.css';
// styles 是一个对象:{ button: '_button_3kx2a_1', primary: '_primary_3kx2a_2', ... }
type ButtonVariant = 'primary' | 'danger';
interface ButtonProps {
variant?: ButtonVariant;
disabled?: boolean;
children: React.ReactNode;
onClick?: () => void;
}
function Button({ variant = 'primary', disabled, children, onClick }: ButtonProps) {
// 拼接多个类名
const className = [
styles.button,
styles[variant],
disabled ? styles.disabled : '',
].filter(Boolean).join(' ');
return (
<button
className={className}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
composes — 组合样式
/* 用 composes 复用已有类(类似继承)*/
.base {
padding: 8px 16px;
border-radius: 6px;
}
.primaryBtn {
composes: base; /* 继承 base 的所有样式 */
background: #3178C6;
color: white;
}
/* 还可以跨文件 composes */
.specialBtn {
composes: flexCenter from './utils.module.css';
font-weight: 700;
}
TypeScript 类型支持
默认情况下 CSS Modules 导入类型为 any。使用 typed-css-modules 可以生成精确的类型定义文件,让编辑器提供类名自动补全和错误检查。
# 生成 .module.css.d.ts 类型声明文件
npx tcm src --watch
# 生成后,Button.module.css.d.ts 内容类似:
// declare const styles: { button: string; primary: string; danger: string; };
// export default styles;
4. Tailwind CSS 详解
Tailwind CSS 是"功能类优先"(Utility-First)的 CSS 框架。它不提供 .btn 这样的预制组件类,而是提供大量原子类(p-4 text-blue-500 flex),每个类只做一件事。
安装配置(Vite + Tailwind v4)
# 安装 Tailwind(v4 使用新的配置方式)
npm install tailwindcss @tailwindcss/vite
# vite.config.ts
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react(), tailwindcss()],
});
/* src/index.css — 只需一行 */
@import 'tailwindcss';
常用类分类速查
// 布局
flex items-center justify-between // Flexbox
grid grid-cols-3 gap-4 // Grid
w-full h-screen max-w-lg // 尺寸
// 间距
p-4 // padding: 1rem (16px)
px-6 py-2 // padding x轴/y轴
mt-8 mb-4 // margin top/bottom
// 颜色与背景
text-gray-900 bg-white // 白色背景黑色文字
text-blue-500 bg-blue-50 // 蓝色系
border border-gray-200 // 边框
// 字体
text-sm text-base text-lg text-xl // 字号
font-normal font-medium font-bold // 字重
leading-relaxed tracking-wide // 行高/字间距
// 圆角与阴影
rounded rounded-lg rounded-full // 圆角
shadow shadow-md shadow-lg // 阴影
// 交互
hover:bg-blue-600 focus:ring-2 // 悬停和聚焦
cursor-pointer select-none // 鼠标样式
transition duration-200 // 过渡动画
实战示例:用 Tailwind 写 Card 组件
// 用 Tailwind 类名直接在 JSX 中编写样式
function UserCard({ user }: { user: User }) {
return (
<div className="bg-white rounded-xl shadow-md p-6 flex items-center gap-4 hover:shadow-lg transition-shadow">
<img
src={user.avatar}
alt={user.name}
className="w-12 h-12 rounded-full object-cover"
/>
<div>
<h3 className="text-base font-semibold text-gray-900">{user.name}</h3>
<p className="text-sm text-gray-500">{user.email}</p>
</div>
</div>
);
}
cn() 工具函数:条件类名的最优解
在 React 中经常需要根据条件拼接 Tailwind 类名。clsx 处理条件合并,tailwind-merge 解决 Tailwind 类冲突(如同时有 p-2 和 p-4,后者优先)。
# 安装
npm install clsx tailwind-merge
// lib/utils.ts — 定义 cn 工具函数
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// 使用示例
cn('p-2', 'p-4') // → 'p-4'(冲突解决)
cn('base-class', isActive && 'active') // → 条件添加
cn({ 'text-red-500': hasError, 'text-green-500': isSuccess })
组件变体:cva(class-variance-authority)
当组件有多个变体(variant/size/color)时,手动拼字符串很容易失控。cva 提供结构化的变体管理,配合 TypeScript 有完整的类型提示。
// npm install class-variance-authority
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../lib/utils';
// 定义按钮变体
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-blue-600 text-white hover:bg-blue-700',
outline: 'border border-gray-300 hover:bg-gray-100',
ghost: 'hover:bg-gray-100 text-gray-700',
danger: 'bg-red-500 text-white hover:bg-red-600',
},
size: {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4 text-base',
lg: 'h-12 px-6 text-lg',
},
},
defaultVariants: {
variant: 'default',
size: 'md',
},
}
);
// 组件 Props 从 cva 推断类型
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
function Button({ variant, size, className, ...props }: ButtonProps) {
return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />;
}
// 用法(TypeScript 会提示合法值)
// <Button variant="danger" size="lg">删除</Button>
5. 响应式设计
Tailwind 使用移动优先的断点前缀。不带前缀的类名应用于所有屏幕,带前缀的类名只在指定断点及以上生效。
| 前缀 | 最小宽度 | 对应设备 |
|---|---|---|
| (无前缀) | 0px | 所有设备(移动优先) |
sm: | 640px | 小屏手机横屏 |
md: | 768px | 平板 |
lg: | 1024px | 笔记本 |
xl: | 1280px | 桌面显示器 |
2xl: | 1536px | 大显示器 |
// 移动:单列;平板:两列;桌面:三列
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
...
</div>
// 移动:隐藏;桌面:显示
<aside className="hidden lg:block">侧边栏</aside>
// 移动:竖排;桌面:横排
<nav className="flex flex-col md:flex-row gap-2 md:gap-6">...</nav>
6. 深色模式
Tailwind 的 dark: 前缀配合 HTML 根元素的 class="dark" 即可实现深色模式切换。
// tailwind.config(v3)或直接在 v4 中开箱即用
// darkMode: 'class' — 通过 class 切换(而非系统偏好)
// 组件:深色下切换背景和文字颜色
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<h1 className="text-2xl font-bold">标题</h1>
</div>
// ThemeToggle.tsx — 切换深色/浅色模式
import { useState, useEffect } from 'react';
function ThemeToggle() {
const [dark, setDark] = useState(() =>
localStorage.getItem('theme') === 'dark'
);
useEffect(() => {
// 在 html 根元素上切换 class
document.documentElement.classList.toggle('dark', dark);
localStorage.setItem('theme', dark ? 'dark' : 'light');
}, [dark]);
return (
<button
onClick={() => setDark((d) => !d)}
className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800"
>
{dark ? '☀️ 浅色' : '🌙 深色'}
</button>
);
}
7. Headless UI:无样式组件库
Headless UI(无头组件库)提供完全可访问的、功能完整的组件,但不带任何样式。你可以用 Tailwind 或任何方式来定义外观。核心理念:逻辑与样式分离。
主流选择:
- Radix UI:低级别 UI 原语(Primitives),高度可组合,生态最好,shadcn/ui 基于此构建
- Headless UI:Tailwind 官方出品,与 Tailwind 搭配最顺滑
- Ariakit:WAI-ARIA 合规性最严格
// npm install @radix-ui/react-dialog
import * as Dialog from '@radix-ui/react-dialog';
// 使用 Radix Dialog + Tailwind 样式
function MyDialog() {
return (
<Dialog.Root>
<Dialog.Trigger className="px-4 py-2 bg-blue-600 text-white rounded-lg">
打开对话框
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm" />
<Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-xl p-6 w-full max-w-md shadow-xl">
<Dialog.Title className="text-lg font-bold mb-2">标题</Dialog.Title>
<Dialog.Description className="text-gray-600">描述内容</Dialog.Description>
<Dialog.Close className="absolute top-4 right-4 text-gray-400 hover:text-gray-700">
✕
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}