
Tailwind CSS 最大的价值之一,不只是它提供了大量现成的原子类,而是它帮我建立了一套稳定的样式思维方式。开发 UI 时,我不再反复纠结“这个组件该起什么 class 名”“这段样式应该写在什么层级”,而是围绕布局、间距、颜色、状态去直接表达界面。
Tailwind CSS 最大的价值之一,不只是它提供了大量现成的原子类,而是它帮我建立了一套稳定的样式思维方式。开发 UI 时,我不再反复纠结“这个组件该起什么 class 名”“这段样式应该写在什么层级”,而是围绕布局、间距、颜色、状态去直接表达界面。
TW真是一个伟大的创意!
但真实项目不会永远停留在“把原子类堆上去就好”的阶段。
当页面里开始出现一组外观样式高度一致、但结构又不完全值得抽成组件的元素时,样式组织会变成一个新的问题:
这篇文章就讨论一个非常具体的问题:在 Tailwind CSS v4 中,如何在不破坏 utility-first 工作流的前提下,定义可复用样式。
我会结合 block-style-cms 里的一个实际场景,说明为什么我最终选择在全局样式中定义自定义 utility,以及这种做法的收益、场景和注意事项。
在 block-style-cms 中,用户联系方式区域存在多个输入框。它们承载的字段不同,但视觉表现基本一致。

如果完全按照 Tailwind 的原子类写法,每个输入框可能都会带上这样一串 class:
这当然没有问题,而且在很多场景下,这就是最直接、最高效的写法。
问题在于,当同一组样式在多个位置重复出现后,后续维护成本会逐渐变高:
这时真正要解决的问题已经不是“Tailwind 能不能写出来”,而是“如何在 Tailwind 的处理生态中,优雅地管理重复样式”。
在这个场景里,我大致评估过三种方案。
这是最符合 Tailwind 默认工作流的方式。
优点很明显:
但它的缺点也很明确:
如果这些输入框不仅样式一致,结构、交互和行为也一致,那么抽组件通常是正确选择。
但这个场景里,我要解决的核心并不是组件抽象,而是样式抽象。
如果只是为了复用这几个 className 就封装一个组件,往往会让组件承担过多职责:
换句话说,组件复用解决的是结构问题,不一定适合单纯的样式复用问题。
这是我最终选择的方案。
它的核心思路不是“脱离 Tailwind 单独写 CSS”,而是“在 Tailwind 的设计系统里扩展 Tailwind”。
这种方式特别适合下面这类场景:
当前项目使用 Next.js,整体样式入口是 globals.css(有些项目中可能为app.css或者styles.css)。这正好是定义项目级可复用样式的合适位置。
在 Tailwind CSS v4 中,官方更推荐使用 @utility 来定义自定义工具类。相比旧语境下的 @layer utilities 写法,@utility 的语义更直接,也更符合 v4 的 CSS-first 配置方式。
例如,我把上面的重复输入框样式整理成一个 utility:
使用时,组件里的 className 就会干净很多:
如果还需要局部覆写,也仍然可以继续叠加原子类:
这篇文章的重点不只是“可以这么写”,而是“为什么这在 v4 里是合理的”。
Tailwind CSS v4 的一个重要变化,是它更强调 CSS-first 配置和样式变量。像 --spacing、--radius-lg、--color-gray-300 这样的变量,本质上就是项目设计系统的一部分。
这意味着当你通过这些变量去定义自定义 utility 时,你并没有脱离 Tailwind,反而是在复用它提供的统一设计基础。
这种做法有几个直接好处。
这点非常重要。
如果你完全在外部再写一套普通 CSS 值,比如手动写死 8px、12px、#d1d5db,短期内也能工作,但长期看,它会慢慢形成第二套样式来源。
而基于 v4 的主题变量来定义 utility,则意味着:
将来如果主题有调整,这些 utility 也能自然保持一致性。
原本散落在多个 JSX 文件里的 className,被收敛成一个统一入口。
之后无论是边框颜色、输入框 padding,还是 focus 状态,只需要改一处,而不是在多个组件中搜索替换。
这也是我在业务代码里非常看重的一点。
当 JSX 中堆着一长串样式时,你虽然知道这些都是 Tailwind class,但业务语义会被埋掉。把重复视觉规则抽到 utility 之后,组件本身会更聚焦于结构和行为。
很多时候,我们会下意识把“重复样式”理解成“应该抽组件”。
但这两件事并不总是等价。
自定义 utility 提供了一种中间层方案:
对真实业务开发来说,这通常是一个很实用的平衡点。
从结果上看,自定义 utility 确实也是一个 CSS 类。但我想强调的是,这种写法和“完全脱离 Tailwind 写一套样式”并不是一回事。
区别在于:我不是把它当成传统语义化 CSS 的回归,而是把它当成 Tailwind 体系内的一层项目级封装。
如果写法仍然围绕 Tailwind v4 的设计变量展开,那么它本质上仍然服从同一套设计语言。
可以用一个简单的对比来理解:
这也是为什么我更倾向于把它理解成“在 Tailwind 内扩展 Tailwind”,而不是“绕过 Tailwind”。
任何抽象一旦离开边界,就会开始制造新的问题。
我认为,自定义 utility 适合下面这些情况:
相反,下面这些情况就不一定适合:
一个简单判断标准是:
如果你复用的是“视觉规则”,优先考虑 custom utility;如果你复用的是“结构和行为”,优先考虑组件。
在定义 custom utility 时,写的是原生 CSS 规则,而不是再次在 CSS 里拼接 Tailwind 原子类。
也就是说,这里要复用的是 Tailwind 的设计令牌和体系,而不是把 HTML 里的 utility class 直接搬进 CSS。
这一点很重要,因为它决定了你的抽象是不是仍然清晰:
如果两者边界不清,后续很容易重新回到难以维护的样式组织方式。
Tailwind CSS 的目标,从来不是拒绝抽象,而是避免失控的抽象。
当项目里开始出现重复样式时,最直接的方案往往是继续写原子类;但当重复达到一定程度后,仅靠复制 className 已经不再是最优解。
在 Tailwind CSS v4 中,使用 @utility 定义自定义工具类,是一种很实用的中间方案:
对我来说,这类方式最有价值的地方,不只是代码变短了,而是它让我在真实项目里能更稳定地管理样式增长。
如果你的项目里也开始出现“同类样式反复出现,但又暂时不值得抽组件”的情况,那么在 Tailwind CSS v4 中定义 custom utility,会是一个值得优先考虑的方案。