I Don't Like Tailwind. Sorry, Not Sorry

我知道 Tailwind 很火。我知道很多人喜欢它。我知道写篇文章说"我不喜欢 Tailwind"会招来很多反驳。

但我还是想说。

这不是一个"Tailwind 好还是不好"的技术讨论,是一个"为什么它不适合我"的主观判断。主观判断不需要被证明,只需要被表达。

我的判断:Utility-first CSS 是一个有缺陷的设计理念,Tailwind 是这个理念最流行的实现,但它解决的是小问题,引入了更大的问题

先说 Tailwind 解决了什么问题

Tailwind 的核心主张:把 CSS 属性变成原子化的 utility class,用 HTML 的 class 属性来控制样式。

<!-- 传统 CSS -->
<div class="card">
  <h2 class="card-title">Title</h2>
  <p class="card-description">Description</p>
</div>

<style>
.card {
  padding: 1.5rem;
  border-radius: 0.5rem;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.card-title {
  font-size: 1.25rem;
  font-weight: 600;
  margin-bottom: 0.5rem;
}
.card-description {
  color: #6b7280;
  line-height: 1.5;
}
</style>

<!-- Tailwind -->
<div class="p-6 rounded-lg shadow-md">
  <h2 class="text-xl font-semibold mb-2">Title</h2>
  <p class="text-gray-500 leading-relaxed">Description</p>
</div>

Tailwind 解决的是CSS 管理问题:不用在文件和 HTML 之间跳转,不用想一个 class 要叫什么名字,不用担心 class 命名冲突。

这是真实的问题。很多人确实在和 CSS 管理搏斗。

BEM、SACSS、OOCSS……这些方法论我用过,最后要么是 class 名字越来越长,要么是嵌套越来越深,要么是"这个样式到底该写在哪个文件里"的选择困难症。

Tailwind 的思路是:别想了,所有样式都是 utility class,直接拼在 HTML 里。

我理解这个思路产生的背景。CSS 确实难管。但我不认为 utility-first 是正确的方向。

但我看到的是这些问题

问题一:HTML 变得臃肿且难以阅读

看看这个真实项目里的按钮组件:

<!-- Tailwind 版本的按钮,来自 shadcn/ui 的真实代码 -->
<button class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2">
  <svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v14m7-7H5" />
  </svg>
  Create New Project
</button>

我看了 30 秒才找到按钮的文字是"Create New Project"。

这不只是我眼力不好的问题。这是信息密度的问题。HTML 原本应该是语义化的、结构清晰的。现在它变成了 utility class 的堆砌。

对比一下语义化的 HTML + CSS 的版本:

<button type="button" class="btn btn-primary" aria-label="Create new project">
  <Icon name="plus" />
  <span>Create New Project</span>
</button>

哪个更容易理解?显然是第二个

我面试过用 Tailwind 的开发者,让他们解释一个按钮的行为。他们通常只能说"点击会 hover,颜色变深"。但如果你问他们"为什么是这个 hover 颜色",他们往往答不上来——因为那是 Tailwind 配置的,不是他们设计的。

这不怪他们。这是 Tailwind 的设计导致的:开发者不需要知道设计,只需要知道 utility。

问题二:设计系统的一致性无法自动保证

Tailwind 的卖点之一是"通过约束实现一致性"——你只能用 Tailwind 提供的颜色、间距,所以不会有"这个 #ffcc00 到底是品牌黄还是错误黄"的困惑。

但这个逻辑有个问题:约束不是一致性,保证约束才是一致性

用 Tailwind:

<!-- 你可以选择 brand-blue -->
<button class="bg-blue-600">OK</button>
<!-- 你也可以选择 sky-500 -->
<button class="bg-sky-500">OK</button>
<!-- 你甚至可以选择 teal-600 -->
<button class="bg-teal-600">OK</button>

这三个颜色可能在你的设计系统里是不可接受的,但 Tailwind 不会阻止你

Tailwind 的约束是"你只能从我提供的颜色里选",不是"你只能选我允许的那个颜色"。

用 CSS 变量 + 设计 token:

/* 定义清楚 */
:root {
  --color-action: #2563eb;
  --color-action-hover: #1d4ed8;
  --color-danger: #dc2626;
  --spacing-button-padding-x: 1rem;
  --spacing-button-padding-y: 0.5rem;
  --radius-button: 0.375rem;
}

.btn-primary {
  background: var(--color-action);
  padding: var(--spacing-button-padding-y) var(--spacing-button-padding-x);
  border-radius: var(--radius-button);
}

.btn-danger {
  background: var(--color-danger);
  padding: var(--spacing-button-padding-y) var(--spacing-button-padding-x);
  border-radius: var(--radius-button);
}

如果你定义了设计系统,用 CSS 变量,你就不可能不小心用了不在系统里的颜色。如果你没定义设计系统,Tailwind 和普通 CSS 一样会乱。

问题三:响应式设计在 Tailwind 里是反直觉的

Tailwind 的响应式前缀是它的一个卖点:

<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  <!-- 移动端 1 列,平板 2 列,桌面 3 列 -->
</div>

但这个写法的背后有一个问题:断点写在 class 里,意味着你必须在 HTML 里做决策

断点是技术实现细节,而 HTML 应该是语义结构

用 CSS 的写法:

.grid {
  display: grid;
  grid-template-columns: repeat(1, 1fr);
  gap: 1rem;
}

@media (min-width: 768px) {
  .grid { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 1024px) {
  .grid { grid-template-columns: repeat(3, 1fr); }
}

HTML:

<div class="grid"><!-- 内容 --></div>

断点在哪里是 CSS 的责任,不是 HTML 的。Tailwind 把这个责任推给了写 HTML 的人。

更糟糕的是:当你需要调整平板端的列数时,你需要去 HTML 文件里找到这个 div,改掉 md:grid-cols-2。如果这个 div 在 10 个页面里复用,你得改 10 个地方。

CSS 的方式,只需要改一个地方的 media query。

问题四:Hover、Focus 这些状态是全局的,不是个性化的

<!-- Tailwind 写法 -->
<button class="bg-blue-500 hover:bg-blue-700 focus:ring-2 focus:ring-blue-300">
  Submit
</button>

这里的问题:hover/focus 样式写在 HTML 里,但它们是交互状态,是行为,不是内容

交互状态应该是"这个元素在 hover 时表现如何",而不是"这个按钮的文本在 hover 时表现如何"。

CSS 的写法:

.btn-submit {
  background: var(--color-submit);
}

.btn-submit:hover {
  background: var(--color-submit-hover);
}

.btn-submit:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

HTML:

<button type="submit" class="btn-submit">Submit</button>

语义清晰,职责分离,状态管理和内容管理是分开的

如果有一天设计系统要改 focus ring 的样式,CSS 的方式只要改一处。Tailwind 的方式要在每一个 focus 的元素上改 class。

问题五:组件复用 vs utility 复用的权衡

Tailwind 的支持者会说:你应该把重复的 class 提取成组件,不是提取成 CSS class。

// Tailwind 的组件提取方式:JavaScript 函数
function Button({ children, variant = 'primary' }) {
  const baseClasses = 'inline-flex items-center justify-center rounded-md text-sm font-medium'
  const variantClasses = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700',
    secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
    ghost: 'hover:bg-gray-100 text-gray-700'
  }

  return (
    <button class={`${baseClasses} ${variantClasses[variant]}`}>
      {children}
    </button>
  )
}

但这又引出了另一个问题:你引入了 JavaScript 来管理样式,而 CSS 原本就是来做这个的

CSS class 本身就是组件。.btn 是一个组件,btn-primary 是这个组件的变体。这不需要 JavaScript 函数来封装。

.btn {
  /* base styles */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 0.375rem;
  font-size: 0.875rem;
  font-weight: 500;
  padding: 0.5rem 1rem;
}

.btn-primary {
  background: var(--color-action);
  color: white;
}

.btn-primary:hover {
  background: var(--color-action-hover);
}

.btn-secondary {
  background: var(--color-gray-200);
  color: var(--color-gray-900);
}

CSS 的力量在于选择器,用好了选择器,不需要 JavaScript 来做样式封装

我承认 Tailwind 有优点的场景

说清楚我的立场:我不是说 Tailwind 一无是处。它在某些场景下是合理的选择。

场景一:原型开发

快速出原型的时候,用 Tailwind 是高效的。你不需要定义设计系统,不需要写 CSS,直接在 HTML 里堆 class 能跑就行。

但这是原型,原型不追求可维护性,原型追求速度

原型做完了,最终要重构。重构的时候,很多人选择把 Tailwind 改成 CSS——因为 Tailwind 原型的维护成本通常比 CSS 原型更高。

场景二:没有设计系统的团队

一个 3 个人的小团队,没有设计师,没有设计系统,用 Tailwind 的约束来避免配色混乱,是合理的。

但这有两个问题:

  1. 这是团队没有设计系统的问题,不是 Tailwind 解决的问题。更好的做法是建立设计系统,然后选择合适的 CSS 方案。

  2. Tailwind 本身不是设计系统。它只是让你在它提供的约束里做选择。如果你需要一个完整的设计系统,你还是要在 Tailwind 配置里定义一堆变量——跟 CSS 变量没有本质区别。

场景三:Tailwind + 组件库

配合 shadcn/ui 这样的组件库用,开发者只使用预设的组件,不自己写 class。这种情况下,Tailwind 的约束是有意义的。

但这又引出了另一个问题:你是在用 Tailwind,还是在用 shadcn/ui 的设计系统?

shadcn/ui 把设计系统做成了预设的组件,你可以不用知道 Tailwind 写了什么。事实上,shadcn/ui 的文档强调的是"你可以复制粘贴代码,不需要安装包"——这本质上是说"你可以不管 Tailwind"。

如果 shadcn/ui 是你用 Tailwind 的主要原因,那你其实是在用 shadcn/ui,不是 Tailwind

场景四:Markdown 渲染的内容

在 CMS 里渲染 Markdown 输出的 HTML,Tailwind 的 Typography 插件(@tailwindcss/typography)很好用:

<article class="prose prose-lg prose-slate">
  <!-- Markdown 输出的 HTML -->
</article>

这是 Tailwind 的正确使用方式:对于你无法控制的输入(Markdown 输出的 HTML),用全局的样式来格式化。

但这是 Tailwind 的 Typography 插件,不是 utility-first 的胜利。

我的核心问题:Utility-first 是一个反模式

Utility-first 的问题是它把内容(HTML 的语义)表现(CSS 的属性)混在了一起。

这不是 Tailwind 的创新,这是退步——因为 Web 标准花了 20 年,才把内容(HTML)和表现(CSS)分开。

HTML 负责语义:这是标题,这是段落,这是按钮。
CSS 负责表现:标题用什么字体,段落用什么颜色,按钮用什么尺寸。

这是正确的分层。正确的分层带来的好处是:HTML 作者和 CSS 作者可以是不同的人,他们的工作可以独立进行。

Utility-first CSS 把这个分层打破了。它把"padding: 1rem"这个表现属性,直接写进了 HTML 的 class 里。

从 HTML 的角度,这就是行内样式

<!-- Tailwind -->
<div class="p-6">

<!-- 等价于行内样式 -->
<div style="padding: 1.5rem;">

Utility class 看起来不是行内样式,但它做的事情是一样的——把表现和内容混在一起。

区别只是:行内样式是 <div style="padding: 1.5rem;">,Tailwind 是 <div class="p-6">

如果你反对行内样式,你没有理由支持 utility-first CSS——除非你只是觉得行内样式写起来更丑。

我的团队观察

我在两个用 Tailwind 的团队工作过,观察到一些共同的问题:

问题一:新人的学习曲线比预期的高

Tailwind 的文档说"不需要学 CSS"。实际上:你需要学 CSS,但你要学的是 Tailwind 的 CSS,不是标准 CSS

新人问我:"为什么 rounded-lg 是 0.5rem?"我得告诉他们去查 Tailwind 的配置。
新人问:"为什么 hover:bg-primary 可以但 active:bg-primary 不行?"我得告诉他们去查 Tailwind 支持的变体。
新人问:"为什么这个按钮在深色模式下颜色不对?"我得告诉他们去配置 Tailwind 的 dark mode。

这些不是 Tailwind 特有的问题,CSS 也有这些问题。但标准 CSS 有 MDN 可以查,有 Stack Overflow 可以搜,有无数的教程。Tailwind 的文档很全,但离开了 Tailwind 生态,这些知识基本没用。

问题二:设计师和产品经理解的"设计系统"和 Tailwind 的"设计系统"不是同一个东西

设计师说"我们需要一个设计系统",他们想要的是:颜色规范、间距规范、字体规范、组件库。

Tailwind 给你的是:预设的颜色(你可以改配置)、预设的间距(你可以改配置)、预设的字体(你可以改配置),加上大量 utility class。

这两个"设计系统"完全不同。设计师给的设计系统是规范文档,Tailwind 给的是一个工具箱。

规范文档告诉你应该做什么,工具箱告诉你能做什么。这是两种不同的思维方式。

问题三:代码审查变成了 class 审查

<!-- Review 1 -->
<div class="p-6 rounded-lg shadow-md">

<!-- Review 2: 阴影不够深 -->
<div class="p-6 rounded-lg shadow-lg">

<!-- Review 3: 阴影太深,改成 shadow-md,但 p-4 更好 -->
<div class="p-4 rounded-lg shadow-md">

<!-- Review 4: p-4 太小了,用 p-6,但圆角要改吗? -->
<div class="p-6 rounded-xl shadow-md">

<!-- ... -->

在 Tailwind 项目里,代码审查很容易变成审美讨论,因为每个开发者都有自己的审美偏好。

CSS 的方式更清晰:设计师定义了 spacing scale,开发者在 CSS 变量里选择。这是设计决策,不是开发者决策。

我见过的 Tailwind 项目走向

观察过多个 Tailwind 项目的演进,总结下来有三条路:

路一:成功(少数)
- 团队小(<5人)
- 有明确的设计规范
- 主要使用预设组件
- 不自己写太多 class

路二:失控(多数)
- 团队大(>10人)
- 没有统一的设计规范
- 每个开发者都有自己的 class 风格
- !important 开始出现
- "重构"变成了"重写"

路三:放弃(一些)
- 回到 CSS Modules 或 CSS-in-JS
- 保留 Tailwind 用于原型
- 混合使用(Tailwind + CSS)

我的结论

我不是 Tailwind 的目标用户。

Tailwind 适合:
- 原型开发(追求速度)
- 没有设计系统的小团队(用约束代替规范)
- 需要快速交付的 CMS 主题(不需要长期维护)
- 喜欢在 HTML 里思考样式的开发者(个人偏好)

Tailwind 不适合我,因为:
- 我在乎 HTML 的可读性(这是给团队其他人看的)
- 我在乎设计系统的一致性(规范 > 约束)
- 我在乎 CSS 和 HTML 的职责分离(这是正确的分层)
- 我在乎团队里新人的学习曲线(标准 CSS 比 Tailwind 更容易找到资源)

这不是对错的问题,是适合不适合的问题。

但我要说一句:那些说"Tailwind 是 CSS 的未来"、"所有项目都应该用 Tailwind"的人,你们太乐观了。

CSS 的未来不是 utility-first。CSS 的未来是:更好的 CSS 工具、更强的 CSS 变量系统、更完善的设计 token 规范、以及更清晰的职责分离

Tailwind 是解决方案之一,不是唯一的答案。


如果你喜欢 Tailwind,那很好,它确实解决了你的问题。如果你和我一样不喜欢,那也很好,我们都有自己的审美和偏好。观点不同很正常,互相尊重就好。