CSS 框架
方案分类与选型
组件库风格 vs 原子化风格
| 维度 | 组件库风格 | 原子化风格 |
|---|---|---|
| 代表 | Bootstrap, Ant Design, Element Plus | Tailwind CSS, UnoCSS |
| 设计理念 | 封装好的 UI 组件开箱即用 | 提供最小 CSS 单元,开发者组合构建 |
| 开发体验 | 快速搭建页面,少写样式 | 在 HTML 中直接写类名,无需切文件 |
| 定制性 | 难深度定制(需覆盖/less 变量) | 极高,每一像素都可控 |
| CSS 体积 | 全量引入通常 ~100-200KB+ | 按需生成,生产 ~5-10KB gzipped |
| 学习成本 | 低(熟悉组件 API 即可) | 先高后低(需要记忆类名体系) |
| 设计一致性 | 组件自带设计语言 | 依赖设计令牌(design tokens)配置 |
| 重构成本 | 替换组件库 = 重写 UI | CSS 类名与 HTML 绑定紧密 |
适用场景对比
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 管理后台 / B 端 | Ant Design / Element Plus / Bootstrap | 表单、表格、弹窗等复杂组件开箱即用 |
| C 端品牌站点 | Tailwind CSS | 设计令牌统一,高度定制 |
| SaaS 产品 UI | Tailwind CSS + 组件库 | 用 Tailwind 做布局和通用样式,组件库做交互 |
| 快速原型 / MVP | Bootstrap / UnoCSS | Bootstrap 原型快;UnoCSS 灵活且体积小 |
| 微前端多应用 | CSS Modules / Tailwind | CSS Modules 天然隔离;Tailwind 无冲突 |
| 要求极致性能 | UnoCSS | 无解析阶段,即时生成,更小体积 |
| TypeScript 类型安全 | Panda CSS / Vanilla Extract | 编译时 CSS,TypeScript 编写 |
| 设计系统 / 组件库内部 | Vanilla Extract / Panda CSS | Recipe 模式方便定义组件变体 |
Tailwind CSS
Utility-first 理念
传统语义化 CSS 的问题:
/* 语义化:.card-title 在全局唯一,心智负担重 */
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 0.5rem;
}
/* 改一次样式:改名 / 加修饰符 / 提权,GSS 持续膨胀 */
Utility-first 用组合类替代:
<!-- 无需命名,直接在 HTML 中描述样式 -->
<h2 class="text-xl font-semibold text-gray-900 mb-2">标题</h2>
解决了什么:
- CSS 不再全局增长(每个新页面不增加一行 CSS)
- 命名疲劳消失(不用发明
.primary-button-hover-v2这类名字) - 修改样式无副作用(不会意外影响其他地方)
- 设计约束内置(从 100+ 种灰色中选,而不是任意写
#ddd)
配置文件
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
// --- content: JIT 扫描路径 ---
content: [
'./src/**/*.{js,jsx,ts,tsx,vue}',
'./index.html',
// 注意:动态类名必须完整出现在模板文件中
// ❌ `bg-${color}-500` 会被识别失败,JIT 无法预知 color 值
// ✅ 写成完整字符串:`bg-red-500`、`bg-blue-500`
],
// --- theme: 设计令牌 ---
theme: {
// 覆盖默认值
screens: {
'sm': '640px',
'md': '768px',
'lg': '1024px',
'xl': '1280px',
'2xl': '1536px',
},
colors: {
brand: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a5f',
},
},
spacing: {
0: '0',
1: '0.25rem',
4: '1rem',
18: '4.5rem', // 扩展 18 → 4.5rem
},
// extend: 在默认基础上追加
extend: {
fontFamily: {
display: ['"Playfair Display"', 'serif'],
},
animation: {
'fade-in': 'fadeIn 0.3s ease-in-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
},
},
},
// --- plugins: 社区或自定义插件 ---
plugins: [
require('@tailwindcss/forms'), // 表单样式重置
require('@tailwindcss/typography'), // 文章排版(prose 类)
require('@tailwindcss/aspect-ratio'),
// 自定义插件
function({ addUtilities }) {
addUtilities({
'.scrollbar-hide': {
'scrollbar-width': 'none',
'&::-webkit-scrollbar': { display: 'none' },
},
});
},
],
};
响应式设计
断点前缀(移动优先,未加前缀 = 移动端样式):
<!-- 移动:单列;md:两列;lg:三列 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="p-4">Card</div>
</div>
<!-- 文本随断点变化 -->
<p class="text-sm md:text-base lg:text-lg">响应式文本</p>
<!-- 显示/隐藏 -->
<div class="hidden md:block">桌面端可见</div>
<div class="block md:hidden">移动端可见</div>
container 类:
<!-- 自动居中 + 响应式 max-width -->
<div class="container mx-auto px-4">
<!-- 内容 -->
</div>
<!-- 默认 max-width 与各断点一致(sm:640px, md:768px ...) -->
Dark Mode
策略选择:
// tailwind.config.js
export default {
darkMode: 'class', // 'media' | 'class' | 'selector'
};
| 策略 | 原理 | 适用 |
|---|---|---|
media |
跟随系统 prefers-color-scheme: dark |
简单博客、无切换按钮 |
class |
在 <html> 上切换 .dark 类 |
需要用户切换主题、支持三种模式 |
selector (v3.4+) |
自定义选择器,如 [data-mode="dark"] |
非标准 class 命名 |
<!-- class 策略:dark: 变体 -->
<div class="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100">
<h1 class="text-2xl font-bold">标题</h1>
</div>
<!-- 使用 JS 切换 -->
<script>
document.documentElement.classList.toggle('dark');
// 或结合 localStorage 持久化
</script>
状态变体
<!-- 基础交互 -->
<button class="bg-blue-500 hover:bg-blue-600 focus:ring-2 focus:ring-blue-300
active:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed">
提交
</button>
<!-- group-hover: 父容器悬停时子元素变化 -->
<div class="group relative cursor-pointer">
<img src="thumb.jpg" class="transition-transform group-hover:scale-105" />
<div class="opacity-0 group-hover:opacity-100 transition-opacity">
覆盖层
</div>
</div>
<!-- peer-*: 相邻兄弟元素状态变化 -->
<div class="flex items-center gap-2">
<input type="checkbox" class="peer" id="agree" />
<label for="agree" class="peer-checked:text-green-600">同意协议</label>
<!-- peer 基于 DOM 位置,前一个兄弟元素的状态影响后一个 -->
</div>
<!-- 更多变体 -->
<!-- odd: / even: / first: / last: / visited: / required: / invalid: / placeholder: -->
性能优化(JIT 引擎)
JIT 原理:
传统 Tailwind(v2): 预生成全量数千个类 → 100MB+ CSS → PurgeCSS 修剪 → ~10KB
JIT 引擎(v3+): content 扫描 → 仅生成文件中出现的类 → 即时输出 ~10KB
- 无全量预生成步骤,构建速度与项目大小无关
- 开发环境按需即时编译,改动即生效
- 生产构建自动 tree-shake 未使用的类
content 配置最佳实践:
// ❌ 错误:路径不够广,JIT 遗漏类名
content: ['./src/**/*.js'],
// ❌ 错误:动态拼接,JIT 无法静态分析
<div class={`bg-${color}-500`} />
// ✅ 正确:完整类名字符串
content: ['./src/**/*.{js,ts,jsx,tsx,vue,html}'],
// ✅ 安全动态写法:列出所有可能值
const colors = ['red', 'blue', 'green'];
colors.map(c => `bg-${c}-500`);
生产体积:经过 JIT + gzip,典型项目最终 CSS 在 5-10KB,远小于 Bootstrap 的 ~120KB 全量加载。
@apply 与 @layer
@apply:提取重复的 utility 组合(不要滥用,否则又回到语义化 CSS 的老路):
/* 仅在真的重复很多次且不是变体时使用 */
.btn-primary {
@apply px-4 py-2 bg-blue-500 text-white rounded-lg
hover:bg-blue-600 focus:ring-2 focus:ring-blue-300
transition-colors duration-200;
}
.card-base {
@apply bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200;
}
<button class="btn-primary">按钮</button>
@layer 与层级顺序:
@tailwind base; /* 基础样式 (normalize + 设计令牌变量) */
@tailwind components; /* @apply 组件的样式 */
@tailwind utilities; /* utility 类 (hover/focus/dark: 变体) */
/* 自定义样式需指定层级以保证覆盖顺序正确 */
@layer base {
h1 { @apply text-3xl font-bold; }
}
@layer components {
.card { @apply p-6 rounded-xl shadow; }
}
@layer utilities {
.text-balance { text-wrap: balance; }
}
层级优先级:!important 标记:base < components < utilities。所以 utility 始终能覆盖组件样式。
警告:Vue SFC 中 @apply 不支持 @media 或 @screen 指令,也不能混合 dark: 变体。
任意值
当设计令牌不够用时,用任意值语法突破约束:
<!-- 宽度/高度 -->
<div class="w-[300px] h-[200px]">固定尺寸</div>
<!-- 颜色 -->
<div class="bg-[#1da1f2] text-[#ffd700]">任意颜色</div>
<!-- 网格 -->
<div class="grid-cols-[1fr_2fr]">自定义网格比例</div>
<!-- 阴影 -->
<div class="shadow-[0_4px_14px_0_rgba(0,0,0,0.1)]">自定义阴影</div>
<!-- 计算 -->
<div class="top-[calc(100%-4rem)]">表达式</div>
<!-- CSS 变量 -->
<div class="text-[var(--brand-color)]">引用 CSS 变量</div>
与组件框架集成
React:clsx / cn 模式:
// utils/cn.ts (推荐搭配 tailwind-merge)
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// cn 自动处理条件类名 + 合并冲突
<button className={cn(
'px-4 py-2 rounded-lg font-medium transition-colors',
variant === 'primary' && 'bg-blue-500 text-white hover:bg-blue-600',
variant === 'secondary' && 'bg-gray-200 text-gray-800 hover:bg-gray-300',
disabled && 'opacity-50 cursor-not-allowed',
className // 外部传入的类名
)}>
{children}
</button>
CVA(Class Variant Authority)模式:
import { cva, type VariantProps } from 'class-variance-authority';
const button = cva('rounded-lg font-medium transition-colors', {
variants: {
variant: {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
ghost: 'hover:bg-gray-100',
},
size: {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
});
// 类型安全的 props
type ButtonProps = VariantProps<typeof button> & {
children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
function Button({ variant, size, className, ...props }: ButtonProps) {
return <button className={button({ variant, size, className })} {...props} />;
}
Vue:useCssModule 与 class 绑定:
<template>
<button
:class="cn(
'px-4 py-2 rounded-lg',
$attrs.class,
)"
v-bind="$attrs"
>
<slot />
</button>
</template>
<script setup lang="ts">
import { cn } from '@/utils/cn';
</script>
UnoCSS
按需生成
与 Tailwind JIT 的对比:
| 特性 | Tailwind JIT | UnoCSS |
|---|---|---|
| 生成方式 | 解析 content → 匹配正则 → 生成 CSS | 解析 HTML → 匹配规则 → 即时生成 |
| 预热时间 | 首次启动 ~300-500ms(解析配置+扫文件) | 几乎 0 预热 |
| 热更新 | 每次改配置重新编译 | 规则引擎即时响应 |
| 规则引擎 | 固定规则集(基于配置) | 动态规则(预设+自定义规则函数) |
| 体积 | ~5-10KB gzipped | ~2-5KB gzipped(极轻量) |
// uno.config.ts
import { defineConfig, presetUno, presetAttributify, presetIcons } from 'unocss';
export default defineConfig({
presets: [
presetUno(), // Tailwind/Windi CSS 兼容预设
presetAttributify(), // Attributify 模式
presetIcons(), // 图标预设
],
// 自定义规则
rules: [
[/^m-(\d+)$/, ([, d]) => ({ margin: `${d / 4}rem` })],
],
shortcuts: {
'btn': 'px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600',
},
});
预设系统
| 预设 | 功能 | 体积 |
|---|---|---|
presetUno |
Tailwind/Windi 兼容 API(默认) | ~3KB |
presetAttributify |
属性化 HTML 写法 | ~0.3KB |
presetIcons |
Iconify 图标 + CSS 图标 | ~0.5KB |
presetTypography |
文章排版(类似 @tailwindcss/typography) |
~2KB |
presetWebFonts |
Google Fonts / 自定义字体加载 | ~0.3KB |
presetWind |
Windi CSS 完全兼容 | ~4KB |
presetMini |
最小化预设(仅基础 margin/padding/flex/grid) | ~1KB |
Attributify 模式
<!-- 传统 utility 写法 -->
<button class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">
按钮
</button>
<!-- Attributify 模式(需 presetAttributify) -->
<button bg="blue-500" text="white" p="x-4 y-2" rounded="lg" hover="bg-blue-600">
按钮
</button>
<!-- 分组语法 -->
<div flex="~ col" p="4" border="2 rounded gray-200">
<span text="sm gray-500">标签</span>
</div>
优点:HTML 更结构化,属性和值分离,可读性提升。 缺点:需额外预设;部分编辑器插件支持不足。
图标方案
通过 presetIcons + Iconify(100,000+ 图标),无需手动导入:
<!-- 直接使用 Iconify 图标 i-{集合名}-{图标名} -->
<span class="i-carbon-sun dark:i-carbon-moon text-2xl"></span>
<span class="i-logos-react w-8 h-8"></span>
<span class="i-twemoji-flag-china w-6 h-6"></span>
<!-- 图标文字混排 -->
<button class="flex items-center gap-2 px-4 py-2">
<span class="i-carbon-add"></span>
新建
</button>
原理:UnoCSS 在构建时从 Iconify 数据集提取用到的 SVG 作为 CSS background(或内联),不产生网络请求。
与 Tailwind 对比
| 对比项 | Tailwind CSS | UnoCSS |
|---|---|---|
| 成熟度 | 社区最大、生态最丰富 | 较新、但增长迅速 |
| 性能 | 快(JIT) | 更快(规则引擎) |
| 配置灵活度 | 固定 schema,extend 扩展 | 动态规则函数,无限自定义 |
| 预设丰富度 | 官方 + 社区插件 | 预设体系,内置图标和 attributify |
| 文档质量 | 极好(官方文档 + playground) | 好(较新、中文支持好) |
| IDE 支持 | 全套(VSCode/WebStorm/IntelliJ) | VSCode 插件可用,其他有限 |
| 学习曲线 | 中等(需记住类名) | 类似 Tailwind(兼容其类名) |
| SSR/SSG 兼容 | 通过 content 配置 | 构建时扫描 HTML |
| 图标方案 | 需额外库(Heroicons + @heroicons/react) | 内置 presetIcons + Iconify |
选择建议:如果团队熟悉 Tailwind 且需要稳定生态,选 Tailwind;如果追求极致性能、灵活性和图标方案,选 UnoCSS。
其他方案概述
Bootstrap
| 特性 | 说明 |
|---|---|
| 栅格系统 | .container > .row > .col-*-*,12 列弹性栅格,5 个断点 |
| 组件库 | 导航栏、轮播、模态框、表单、卡片等 50+ 组件(需 JS) |
| Utility API | v5 引入,可用 $utilities: () map 自定义 utility 类 |
| 定制方式 | SCSS 变量覆盖 $primary $border-radius |
| 体积 | 全量 ~120KB gzipped(JS + CSS),Tree-shake 困难 |
| 适用场景 | 快速原型、内部系统、对设计无特别要求的团队 |
<!-- Bootstrap 典型用法 -->
<div class="container">
<div class="row">
<div class="col-12 col-md-6 col-lg-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">卡片标题</h5>
<p class="card-text">内容</p>
<a href="#" class="btn btn-primary">按钮</a>
</div>
</div>
</div>
</div>
</div>
CSS-in-JS
| 方案 | 运行时 | 类型安全 | 体积 | 代表用法 |
|---|---|---|---|---|
| styled-components | 运行时注入 | 可选 | ~15KB gzipped | 模板字面量 |
| Emotion | 运行时注入 | 可选 | ~10KB gzipped | css prop |
| Panda CSS | 编译时(零运行时) | 原生 | ~0KB runtime | Recipe / sva |
| Vanilla Extract | 编译时(零运行时) | 原生 | ~0KB runtime | style / recipe / sprinkles |
| Linaria | 编译时提取 | 弱 | ~0KB runtime | 模板字面量编译时 |
styled-components(运行时):
import styled from 'styled-components';
const Button = styled.button<{ $variant: 'primary' | 'secondary' }>`
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
background: ${props => props.$variant === 'primary' ? '#3b82f6' : '#e5e7eb'};
color: ${props => props.$variant === 'primary' ? 'white' : '#374151'};
&:hover {
opacity: 0.9;
}
`;
// 使用
<Button $variant="primary">点击</Button>
运行时 CSS-in-JS 的问题:
- 增加 JS bundle 体积(~10-15KB)
- SSR 需要额外配置(收集样式注入 HTML)
- 运行时解析标签模板字面量有性能开销
- Suspense / Concurrency 模式下可能出现样式闪烁
Panda CSS(编译时零运行时):
// 定义 recipes
import { cva } from '@/styled-system/css';
const button = cva({
base: {
display: 'inline-flex',
alignItems: 'center',
borderRadius: 'lg',
fontWeight: 'medium',
},
variants: {
variant: {
primary: { bg: 'blue.500', color: 'white' },
secondary: { bg: 'gray.200', color: 'gray.800' },
},
size: {
sm: { px: '3', py: '1.5', fontSize: 'sm' },
md: { px: '4', py: '2', fontSize: 'base' },
},
},
});
// 使用 ← 编译时生成原子化 class,运行时无开销
<button className={button({ variant: 'primary', size: 'md' })} />
Vanilla Extract(编译时零运行时):
// styles.css.ts (TypeScript 文件编译为 CSS)
import { style, recipe } from '@vanilla-extract/css';
import { sprinkles } from './sprinkles.css';
export const container = style([
sprinkles({ padding: 'medium', background: 'surface' }),
{
maxWidth: '1200px',
margin: '0 auto',
},
]);
export const button = recipe({
base: sprinkles({ borderRadius: 'md' }),
variants: {
color: {
primary: sprinkles({ background: 'brand', color: 'white' }),
secondary: sprinkles({ background: 'neutral' }),
},
},
});
CSS Modules
/* Card.module.css */
.card {
background: white;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.title {
font-size: 1.25rem;
font-weight: 600;
}
/* composes: 组合其他类(类似 @extend) */
.errorCard {
composes: card;
border: 2px solid #ef4444;
}
// React 中导入
import styles from './Card.module.css';
function Card({ title, children }) {
return (
<div className={styles.card}>
<h3 className={styles.title}>{title}</h3>
{children}
</div>
);
}
CSS Modules 特点:
- 自动生成唯一哈希类名(
.Card_card_abc123),天然作用域隔离 - 无运行时开销(编译时处理)
- 与全局 CSS / Tailwind 混用策略:
// 混用:全局 utility + 局部样式
<div className={cn(
styles.card,
'p-4 shadow-md', // Tailwind 全局 utility
className, // 外部传入的自定义
)}>
适用场景:微前端、多团队协作(防止样式冲突)、已有项目渐进迁移。
选型建议
| 需求 | 推荐 | 说明 |
|---|---|---|
| 快速搭建后台管理 | Ant Design / Element Plus / Naive UI | 组件丰富,开箱即用,少写样式 |
| 快速原型 / MVP | Bootstrap / UnoCSS + 预设 | Bootstrap 原型快;UnoCSS 灵活体积小 |
| C 端高度定制设计 | Tailwind CSS | 设计令牌 + utility-first,每一像素可控 |
| 追求极致性能 | UnoCSS | 最小化 CSS 体积 + 极速按需生成 |
| 全栈 + TypeScript | Panda CSS / Vanilla Extract | 编译时 CSS、类型安全、零运行时 |
| 微前端 / 多人协作 | CSS Modules | 编译时作用域隔离,天然防冲突 |
| 设计系统团队 | Vanilla Extract / Tailwind + CVA | Recipe 模式 + 强类型约束 |
| 现有项目渐进改造 | Tailwind CSS + CSS Modules | 全局 utility + 局部作用域混用 |