八年前,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);
}
}
迁移的核心动作是:
- 把 utility class 提取成语义类名——
flex items-center gap-4变成.user-card的布局规则 - 把硬编码值替换成变量——
bg-blue-500变成var(--color-primary),一处改全局生效 - 用 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需要较新的浏览器支持)
迁移检查清单
如果你决定动手,按这个顺序推进风险最低:
- 先建
design-tokens.css——把 Tailwind 配置里的颜色、间距、字号翻译成 CSS 变量,确保视觉不跑偏 - 从最简单的组件开始——比如按钮、标签、卡片,逐个替换,不要一次性全改
- 每替换一个组件,立刻删除对应的 Tailwind class——避免新旧样式共存导致冲突
- 用浏览器 DevTools 对比迁移前后的渲染结果——间距和字号最容易出偏差
- 全部迁移完成后,移除 Tailwind 依赖——确认没有残留的
@apply或配置引用
Julia 花了一周迁移几个小型网站。对于大型项目,可以按模块分批推进,不需要一步到位。
Tailwind 解决了一个真实问题:在开发者不擅长组织 CSS 的阶段,它提供了有效的脚手架。但当你的 CSS 能力已经成长,现代 CSS 又补上了组织工具,回到原生写法反而更自由——类名你定,变量你控,层级你排,不再受 utility class 的命名和粒度约束。这不是倒退,是升级。