TailwindCSS:原子化 CSS 的工程方法
1. 一个后端开发者的第一反应
第一次看到 Tailwind 的代码,多数人的反应是:
<button class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded shadow">
提交
</button>
这不就是 inline style 换了个马甲吗?HTML 里堆一堆样式类,看起来太丑了。
这个疑问合理。但当你维护过 5 年以上的项目就会发现:传统 CSS 的问题不是"难写",是"难删"。
- 有个
.btn-primary类,你不敢改,因为不知道被多少页面用了 - 有个颜色
#2c8eff,全项目散落 40 多处,改主题要逐个替换 - 两个开发者起了同名的类,后加载的覆盖前面的,事故就发生在凌晨
Tailwind 的思路:不再发明类名。你不需要给一个"按钮"起名字,你直接描述它长什么样。
Java 对照:
| 传统 CSS 写法 | 类比 |
|---|---|
写 .user-card 类 + 维护 CSS 文件 |
写一个 UserCardUtil 工具类,封装"怎么画用户卡片" |
别人也写 .user-card |
包冲突、类名冲突、命名风格不统一 |
| Tailwind 写法 | 类比 |
|---|---|
class="flex p-4 rounded shadow" |
直接用 StringUtils.leftPad + StringUtils.substring 这种小工具拼出结果 |
| 不需要命名、不需要维护 CSS 文件 | 小粒度工具组合,一眼看懂 |
原子化 CSS 的本质:把样式从"命名 + 类定义"的层级降到"直接描述"的层级,省掉了"给样式起名字"这一步。
2. in4vue 里的 Tailwind 配置
v4 的配置比 v3 简单一个数量级,几乎不需要配置文件。项目里已经这样接入:
2.1 Vite 插件
vite.config.ts:
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [vue(), tailwindcss()],
})
2.2 CSS 入口
src/style.css:
@import 'tailwindcss';
就这一行。v4 放弃了 v3 的 @tailwind base; @tailwind components; @tailwind utilities; 三段式,改成一条 @import。
2.3 入口引入
src/main.ts:
import './style.css'
v3 vs v4 的关键区别(避免你看到旧教程踩坑):
| 项 | v3 | v4 |
|---|---|---|
| 配置文件 | tailwind.config.js 必需 |
可选,改用 CSS 里 @theme |
| CSS 入口 | @tailwind base/components/utilities |
@import 'tailwindcss' |
| 内容扫描 | content: ['./src/**/*.{vue,ts}'] |
自动检测,零配置 |
| 构建方式 | PostCSS 插件 | 独立引擎(用 Oxide,Rust 实现) |
in4vue 用的是 v4,遇到"v3 教程让我建 tailwind.config.js"的情况,忽略它,直接用 v4 的方式。
3. 核心:四类工具类
Tailwind 的工具类命名规律性极强,记住规律比背 API 重要。
3.1 布局(Layout)
<!-- Flex -->
<div class="flex items-center justify-between gap-4">...</div>
<!-- 等价于:display: flex; align-items: center; justify-content: space-between; gap: 1rem -->
<!-- Grid -->
<div class="grid grid-cols-3 gap-4">...</div>
<!-- 等价于:display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem -->
对照 CSS 基础笔记的 Flex/Grid:Tailwind 只是把你写过的 CSS 属性改了个写法,逻辑完全一致。
3.2 间距(Spacing)
间距用一套统一的 scale:0, 0.5, 1, 2, 3, 4, 6, 8, 10, 12, 16, 20, 24, ...,单位是 0.25rem(默认 4px)。
<div class="p-4">...</div> <!-- padding: 1rem (16px) -->
<div class="px-4 py-2">...</div> <!-- padding: 0.5rem 1rem -->
<div class="m-4">...</div> <!-- margin: 1rem -->
<div class="mt-2">...</div> <!-- margin-top: 0.5rem -->
<div class="space-y-4">...</div> <!-- 子元素垂直间距 1rem -->
前缀速查:
p= padding,m= margint/r/b/l= top/right/bottom/leftx= 水平(left + right),y= 垂直(top + bottom)
3.3 颜色(Colors)
Tailwind 的调色板分 11 个色阶:50, 100, 200, ..., 900, 950。数字越小越浅,越大越深。
<div class="bg-blue-500 text-white">蓝底白字</div>
<div class="bg-gray-100 text-gray-800">浅灰底深灰字</div>
<div class="border border-gray-300">浅灰边框</div>
经验法则:
bg-背景色,text-文字色,border-边框色- 正文用
gray-800,次要文字gray-500,禁用gray-300 - 主色调固定一个(如
blue-500),hover 比默认深一档(blue-600)
3.4 尺寸与排版(Sizing & Typography)
<div class="w-full h-screen">宽 100%,高 1 屏</div>
<div class="max-w-4xl mx-auto">最大宽度 + 水平居中</div>
<p class="text-lg font-semibold leading-relaxed">大号半粗字,行高宽松</p>
常用尺寸:
w-full= 100%,w-screen= 100vwmax-w-4xl= 56rem(896px),常用于文章主体mx-auto在有max-width时实现居中
4. 响应式:移动优先
Tailwind 默认移动优先:不加前缀的样式是所有屏幕的基础样式,md:、lg: 等前缀表示"在该断点及以上生效"。
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
<!--
默认(手机):1 列
≥ 768px(平板):2 列
≥ 1024px(桌面):3 列
-->
</div>
断点默认值:
| 前缀 | 起始宽度 | 设备 |
|---|---|---|
sm: |
640px | 大手机 |
md: |
768px | 平板 |
lg: |
1024px | 笔记本 |
xl: |
1280px | 桌面 |
2xl: |
1536px | 大屏 |
Java 对照:类似 Spring 的 @Profile("dev") / @Profile("prod"),不同条件下启用不同配置——只不过这里的条件是"屏幕宽度"。
5. 状态变体:hover、focus、dark、group
状态前缀的语法和响应式一样——加在类名前面:
<!-- 鼠标悬停 -->
<button class="bg-blue-500 hover:bg-blue-600">按钮</button>
<!-- 聚焦 -->
<input class="border focus:border-blue-500 focus:outline-none" />
<!-- 禁用 -->
<button class="disabled:opacity-50 disabled:cursor-not-allowed">提交</button>
<!-- 组合:移动端文字小,桌面端大,悬停时加深颜色 -->
<h1 class="text-lg md:text-2xl text-gray-700 hover:text-gray-900">标题</h1>
5.1 暗色模式
<div class="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
白天黑夜自动切换
</div>
开启方式(v4 在 CSS 里声明):
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));
然后用 JS 给 <html> 加 class="dark":
document.documentElement.classList.toggle('dark')
这套和 Element Plus 的暗色模式能联动,后续实战篇会写。
5.2 group:父悬停控制子元素
<div class="group hover:bg-gray-100">
<h3 class="text-gray-700 group-hover:text-blue-500">
卡片标题(鼠标移到卡片上时变蓝)
</h3>
</div>
类似 CSS 的 .parent:hover .child,但不用另起一段 CSS。
6. 任意值(Arbitrary Values):规范外的逃生口
Tailwind 的 scale 覆盖了 95% 的场景,剩下 5% 用方括号语法写任意值:
<div class="top-[117px] w-[42.5%] bg-[#3b82f6]">
特殊位置、特殊宽度、特殊颜色
</div>
什么时候用:设计师给了一个非标准数值(如 "这里必须是 117px")。 什么时候别用:正常情况下能用 scale 就用 scale,任意值是逃生口不是日常操作。
7. @apply:把原子类抽成自定义类
极少数情况下,你确实有一组样式反复出现(比如表单错误提示),可以用 @apply 抽象:
/* style.css */
.form-error {
@apply text-sm text-red-500 mt-1;
}
<p class="form-error">用户名不能为空</p>
什么时候用:
- 某组类在 10+ 处重复(低频情况)
- 需要和第三方库配合,对方要求特定类名
什么时候别用:
- "我觉得 class 太长了"——这不是用
@apply的理由,Tailwind 作者原话:"长 class 不是坏事,它把样式和结构放在一起,可读性反而更好" - 滥用
@apply等于回到了传统 CSS,失去了原子化的意义
8. 与 Element Plus 的分工
in4vue 同时用 Tailwind 和 Element Plus,分工原则:
| 场景 | 用谁 | 原因 |
|---|---|---|
| 复杂交互组件(表格、弹窗、下拉框、日期选择器) | Element Plus | 已经做好所有交互、可访问性、键盘操作 |
| 布局(flex、grid、间距、响应式) | Tailwind | 比写 <style> 快 10 倍 |
| 自定义展示组件(卡片、列表项、标签) | Tailwind | 不值得为此引入整个组件库 |
| 颜色、字体、间距的全局统一 | 各自的 token | Element Plus 用 CSS 变量,Tailwind 用 @theme |
反模式:在 <el-button> 上贴一堆 Tailwind 类去覆盖内部样式。
<!-- ❌ 不要这样 -->
<el-button class="!bg-red-500 !text-white !rounded-none">...</el-button>
<!-- ✅ 要么用 Element Plus 的 type -->
<el-button type="danger">...</el-button>
<!-- ✅ 要么用 Tailwind 做个自定义按钮 -->
<button class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600">...</button>
原因:Element Plus 的内部样式有自己的权重和结构,强行覆盖会导致升级时样式崩。
9. 主题定制:v4 的 @theme
v4 抛弃了 tailwind.config.js,改用 CSS 里的 @theme 指令:
@import 'tailwindcss';
@theme {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-600: #2563eb;
--font-family-sans: 'Inter', system-ui, sans-serif;
}
声明后,Tailwind 自动生成 bg-brand-500、text-brand-600、font-sans 这些工具类。
Java 对照:类似 Spring Boot 的 application.yml 配置自定义 bean 名,框架自动扫出来注册。
10. 生产构建的"魔法"
你可能注意到:在 <script setup> 里动态拼接类名可能不生效。
<!-- ❌ 可能失效 -->
<div :class="`bg-${color}-500`">...</div>
原因:Tailwind 构建时扫描源码里出现的完整类名,动态拼接的字符串它看不到,于是那些类没被打进最终 CSS。
解法 1:完整写出
<div :class="color === 'red' ? 'bg-red-500' : 'bg-blue-500'">...</div>
解法 2:预先列出(用 safelist,v4 在 CSS 里)
@source inline("bg-red-500 bg-blue-500 bg-green-500");
Java 对照:类似 Spring 的反射调用需要在 reflect-config.json 里声明才能打进 GraalVM 原生镜像——都是"静态分析看不到动态代码"的问题。
11. 一个完整的真实例子:in4vue 笔记卡片
<template>
<article class="group cursor-pointer rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition hover:border-blue-400 hover:shadow-md dark:border-gray-700 dark:bg-gray-800">
<div class="mb-2 flex items-center gap-2 text-xs text-gray-500">
<span class="rounded bg-blue-50 px-2 py-0.5 text-blue-600 dark:bg-blue-900/40 dark:text-blue-300">
{{ note.category }}
</span>
<time>{{ note.date }}</time>
</div>
<h3 class="mb-2 text-lg font-semibold text-gray-900 group-hover:text-blue-600 dark:text-gray-100">
{{ note.title }}
</h3>
<p class="line-clamp-2 text-sm text-gray-600 dark:text-gray-400">
{{ note.summary }}
</p>
</article>
</template>
拆解:
group + group-hover:— 整个卡片悬停时,标题变蓝dark:— 整套暗色适配line-clamp-2— 超过 2 行自动省略(非常实用)transition— 悬停时平滑过渡rounded-lg / shadow-sm / hover:shadow-md— 轻量卡片的标配组合
整个组件没写一行 CSS。
12. 心智地图:什么时候想到什么
| 需求 | 第一反应 |
|---|---|
| 横排几个东西 | flex items-center gap-4 |
| 三等分布局 | grid grid-cols-3 gap-4 |
| 水平居中 | mx-auto max-w-xl |
| 文字省略 | truncate 或 line-clamp-N |
| 边距 | p-4 / px-4 py-2 / space-y-4 |
| 颜色 | bg-{color}-{500} / text-{color}-{700} |
| 圆角+阴影+边框 | rounded-lg shadow border |
| 响应式 | md: 前缀 |
| 暗色 | dark: 前缀 |
| 悬停/聚焦 | hover: / focus: 前缀 |
写 Tailwind 的节奏:看到设计 → 脑子里拆成"布局 + 间距 + 颜色 + 字体 + 状态" → 对应类名拼出来。前两周会慢,一个月后比写 CSS 快 3 倍。
小练习
- 把
HomePage.vue的笔记列表改成上面第 11 节那种卡片样式(含暗色模式) - 给顶栏做个汉堡菜单:小屏
md:hidden,大屏hidden md:flex - 写个"加载中"占位:
animate-pulse bg-gray-200 h-4 w-32 rounded - 在
style.css里用@theme定义--color-brand-500,然后用bg-brand-500验证生效 - 打开 DevTools → Elements,点一个 Tailwind 类看它对应的原生 CSS —— 你会发现它就是普通 CSS,没有魔法
延伸阅读
- Tailwind CSS 官方文档(v4)
- Tailwind Play(在线试验,不用装环境)
- Tailwind CSS IntelliSense (VS Code)(必装插件,写类名带补全和悬停预览)
- Tailwind v4 Release Notes(v3 → v4 的变化)
- Refactoring UI(Tailwind 作者 Adam Wathan 写的设计指南)