从 Tailwind 回到原生 CSS:一位开发者八年后的重新选择

2026-05-22 16 预计阅读时间:1 分钟
来源:oschina.net AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:10 分钟

八年前,Julia Evans 写过一篇热情洋溢的文章,讲述她发现 Tailwind 时的喜悦。彼时她完全不知道如何组织 CSS,在"一团乱麻"和"Tailwind"之间,她毫不犹豫地选择了后者。Tailwind 帮她搭建了大量小型网站。八年后的今天,她花了一周时间,把几个网站从 Tailwind 迁移到语义化 HTML + 原生 CSS,并把整个过程中的发现记录了下来。

这不是一个"Tailwind 不好"的故事,而是一个"我现在能写好 CSS 了"的故事。

当年为什么选 Tailwind

很多开发者选择 Tailwind 的理由和 Julia 一样:不知道怎么组织 CSS。传统 CSS 写起来容易陷入几种困境——

  • 类名冲突:.button 在两个组件里含义不同,样式互相覆盖
  • 样式散落:改一个组件要在多个 CSS 文件里翻找
  • 层级嵌套失控:.nav .list .item .link .icon 越写越深

Tailwind 用 utility class 把样式直接写在 HTML 上,绕过了这些问题。不需要想类名,不需要翻 CSS 文件,一个 flex items-center gap-4 就搞定了布局。对于不熟悉 CSS 组织方式的人来说,这确实是高效的解法。

但八年过去,CSS 本身进化了,Julia 的能力也进化了。

原生 CSS 已经补上了那些短板

现代 CSS 提供了几个关键特性,让"组织 CSS"这件事不再那么痛苦:

CSS Nesting——终于可以在样式块里嵌套子选择器,不用再写 .nav .list .item 这种长链:

.nav {
  display: flex;
  gap: 1rem;

  .item {
    padding: 0.5rem 1rem;

    &:hover {
      background: #f0f0f0;
    }
  }
}

CSS Custom Properties(变量)——用 --color-primary 替代重复的色值,比 Tailwind 的 text-blue-500 更灵活,因为变量可以在运行时动态切换:

:root {
  --color-primary: #3b82f6;
  --spacing-unit: 0.5rem;
}

.card {
  border: 1px solid var(--color-primary);
  padding: calc(var(--spacing-unit) * 2);
}

:has() 选择器——以前必须靠 JavaScript 判断的父子关系,现在 CSS 自己能处理:

/* 当表单组内有输入框聚焦时,整组高亮 */
.form-group:has(input:focus) {
  outline: 2px solid var(--color-primary);
}

@layer 层级控制——显式声明样式的优先级顺序,从根本上解决覆盖冲突:

@layer base, components, overrides;

@layer base {
  h1 { font-size: 2rem; }
}

@layer components {
  .card-title { font-size: 1.5rem; }
}

@layer overrides {
  .hero-title { font-size: 3rem; }
}

这些特性组合起来,让原生 CSS 的组织能力已经不输 Tailwind 的 utility 模式。

实际迁移:从 utility class 到语义化 CSS

Julia 的迁移过程很务实——不是推翻重来,而是逐个替换。下面用一个典型片段演示迁移思路。

迁移前(Tailwind)

<div class="flex items-center gap-4 p-6 bg-white rounded-lg shadow-md border border-gray-200">
  <img class="w-12 h-12 rounded-full object-cover" src="avatar.jpg" alt="头像">
  <div class="flex-1">
    <h2 class="text-lg font-semibold text-gray-900">用户名</h2>
    <p class="text-sm text-gray-500">描述文字</p>
  </div>
  <button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
    关注
  </button>
</div>

HTML 里塞满了 utility class,读结构要在一堆样式指令中翻找。

迁移后(语义化 HTML + 原生 CSS)

<div class="user-card">
  <img class="user-card__avatar" src="avatar.jpg" alt="头像">
  <div class="user-card__info">
    <h2 class="user-card__name">用户名</h2>
    <p class="user-card__desc">描述文字</p>
  </div>
  <button class="user-card__action">关注</button>
</div>
.user-card {
  display: flex;
  align-items: center;
  gap: var(--spacing-lg);
  padding: var(--spacing-lg);
  background: white;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  border: 1px solid var(--color-border);
}

.user-card__avatar {
  width: 3rem;
  height: 3rem;
  border-radius: 50%;
  object-fit: cover;
}

.user-card__info {
  flex: 1;
}

.user-card__name {
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--color-text);
}

.user-card__desc {
  font-size: var(--text-sm);
  color: var(--color-muted);
}

.user-card__action {
  padding: var(--spacing-sm) var(--spacing-md);
  background: var(--color-primary);
  color: white;
  border-radius: var(--radius-md);
  border: none;
  cursor: pointer;

  &:hover {
    background: var(--color-primary-hover);
  }
}

迁移的核心动作是:

  1. 把 utility class 提取成语义类名——flex items-center gap-4 变成 .user-card 的布局规则
  2. 把硬编码值替换成变量——bg-blue-500 变成 var(--color-primary),一处改全局生效
  3. 用 nesting 简化子元素样式——:hover 直接嵌套在按钮样式块里

HTML 结构立刻变得可读,CSS 也有了明确的归属。

迁移中踩到的实际问题

Julia 在迁移过程中遇到了一些具体挑战,值得提前了解:

Tailwind 的 @apply 反向迁移。有些项目为了减少 HTML 里的 class 数量,会在 CSS 里用 @apply 把 utility 组合起来。迁移时要把这些 @apply 块展开成真正的 CSS 属性,工作量不小。

响应式断点。Tailwind 的 md:flex-row 写法很方便,迁移后需要用 @media 手动写断点规则。不过现代 CSS 的容器查询(@container)在某些场景下比媒体查询更精准:

.user-card {
  display: flex;
  flex-direction: column;

  @container (min-width: 400px) {
    flex-direction: row;
  }
}

设计系统的一致性。Tailwind 默认提供了一套完整的 spacing、color、typography scale。迁移后需要自己定义这套变量体系,否则容易回到"乱写数值"的老路。建议一开始就建好变量文件:

/* design-tokens.css — 迁移前先准备好这一步 */
:root {
  /* 色板 */
  --color-primary: #3b82f6;
  --color-primary-hover: #2563eb;
  --color-text: #111827;
  --color-muted: #6b7280;
  --color-border: #e5e7eb;

  /* 间距阶梯 */
  --spacing-xs: 0.25rem;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --spacing-xl: 2rem;

  /* 字号阶梯 */
  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
  --text-xl: 1.25rem;

  /* 圆角 & 阴影 */
  --radius-sm: 0.25rem;
  --radius-md: 0.5rem;
  --radius-lg: 1rem;
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}

有了这套 tokens,写语义化 CSS 时就不会随意填数值,一致性反而比 utility class 更容易维护——改一个变量,全站同步更新。

什么时候该考虑迁移

Julia 的经历说明了一个前提条件:你得先学会组织 CSS,迁移才有意义。如果现在仍然不知道怎么管理类名和样式层级,强行离开 Tailwind 只会回到八年前的"一团乱麻"。

可以考虑迁移的信号:

  • 项目长期维护,HTML 里的 utility class 已经膨胀到难以阅读
  • 需要频繁调整设计系统(颜色、间距),在 Tailwind 配置里改比在 CSS 变量里改更绕
  • 团队成员已经熟悉现代 CSS 特性(nesting、variables、@layer
  • 项目对运行时性能敏感,希望减少 Tailwind 生成的 CSS 体积

暂时不该迁移的信号:

  • 项目短生命周期,快速出活比长期维护更重要
  • 团队对 CSS 组织方式仍然没有共识
  • 浏览器兼容性要求覆盖旧版浏览器(nesting 和 @layer 需要较新的浏览器支持)

迁移检查清单

如果你决定动手,按这个顺序推进风险最低:

  1. 先建 design-tokens.css——把 Tailwind 配置里的颜色、间距、字号翻译成 CSS 变量,确保视觉不跑偏
  2. 从最简单的组件开始——比如按钮、标签、卡片,逐个替换,不要一次性全改
  3. 每替换一个组件,立刻删除对应的 Tailwind class——避免新旧样式共存导致冲突
  4. 用浏览器 DevTools 对比迁移前后的渲染结果——间距和字号最容易出偏差
  5. 全部迁移完成后,移除 Tailwind 依赖——确认没有残留的 @apply 或配置引用

Julia 花了一周迁移几个小型网站。对于大型项目,可以按模块分批推进,不需要一步到位。

Tailwind 解决了一个真实问题:在开发者不擅长组织 CSS 的阶段,它提供了有效的脚手架。但当你的 CSS 能力已经成长,现代 CSS 又补上了组织工具,回到原生写法反而更自由——类名你定,变量你控,层级你排,不再受 utility class 的命名和粒度约束。这不是倒退,是升级。


相关推荐