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 的问题不是"难写",是"难删"

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)

间距用一套统一的 scale0, 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 -->

前缀速查

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>

经验法则

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>

常用尺寸


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>

什么时候用

什么时候别用


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-500text-brand-600font-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>

拆解

整个组件没写一行 CSS


12. 心智地图:什么时候想到什么

需求 第一反应
横排几个东西 flex items-center gap-4
三等分布局 grid grid-cols-3 gap-4
水平居中 mx-auto max-w-xl
文字省略 truncateline-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 倍。


小练习

  1. HomePage.vue 的笔记列表改成上面第 11 节那种卡片样式(含暗色模式)
  2. 给顶栏做个汉堡菜单:小屏 md:hidden,大屏 hidden md:flex
  3. 写个"加载中"占位:animate-pulse bg-gray-200 h-4 w-32 rounded
  4. style.css 里用 @theme 定义 --color-brand-500,然后用 bg-brand-500 验证生效
  5. 打开 DevTools → Elements,点一个 Tailwind 类看它对应的原生 CSS —— 你会发现它就是普通 CSS,没有魔法

延伸阅读