当设计令牌遇上原子化CSS:一次整合失败的反思与融合之道
个人开发者尝试UnoCSS映射设计令牌,当天失败,但收获了一套务实的分工方案
起点:我有一套完整的设计令牌
我的个人项目 moongate-ui 一开始只有三份CSS文件:
colors.css:浅色/深色双主题,40+语义化颜色变量(--ui-primary、--ui-bg-muted……)layout.css:间距、圆角、阴影、动效、字体、断点等布局令牌main.css:全局组件样式、月晕协议、潮汐触发等核心契约
这套令牌系统不依赖任何框架,任何组件都可以通过 var(--ui-spacing-md)、var(--ui-primary) 获取设计约束。它稳定、直观、可维护。
诱惑:UnoCSS 的轻量与原子化
我听过 Tailwind 的“笨重”名声,但 UnoCSS 号称按需生成、零运行时、超轻量。作为个人开发者,我渴望“不用写CSS类名”的体验——直接在模板里堆 flex p-4 text-center,不用切文件,不用想命名。
于是某个夜晚,我决定:把设计令牌“映射”到 UnoCSS 上,既保留设计系统,又享受原子化书写。
碰撞:一个晚上的挣扎
我写了 uno.config.ts,试图把每个 --ui-* 变量映射成原子类:
colors: {
primary: 'var(--ui-primary)',
muted: 'var(--ui-bg-muted)',
// ...
}
然后在 Button.vue 里把原来的 scoped 样式全部替换成原子类:
<button :class="cn('bg-primary text-white', 'bg-bg-muted', 'border-border-subtle')">
问题很快暴露:
- 类名冗余:
bg-bg-muted、border-border-subtle,读起来像口吃。 - 映射维护成本:CSS 变量改一处,UnoCSS 配置也要改,单一真实源变成两个。
- 条件逻辑爆炸:为了处理
neutral颜色,写color === 'neutral' ? 'text-dim' : 'text-'+color。 - 调试困难:浏览器里看到
bg-bg-muted,得反向查找它对应哪个CSS变量。
当晚我就删掉了所有映射代码,回到了原始方案。
顿悟:设计令牌就是最轻量的原子化框架
看着 bg-bg-muted,我突然问自己:为什么不直接写 background-color: var(--ui-bg-muted) 呢?
我的设计令牌系统本身已经提供了所有设计约束。UnoCSS 宣称的“设计约束”能力,我的CSS变量全都有。它剩下的唯一价值就是“快速书写”——即语法糖。
更深一层:把 --ui-primary 映射成 bg-primary,本质上是在“用 CSS 封装 CSS”。UnoCSS 在编译阶段生成 .bg-primary { background-color: var(--ui-primary); } 这样的代码。既然最终都是这行 CSS,我直接在 scoped 样式里写它,不是更直接吗?
反思:原子化CSS与设计令牌并非对立
我的经历并不证明原子化CSS不好,而是证明:当你的项目已经拥有一套成熟的设计令牌时,强行把令牌映射成原子类是多余的。
但这不等于要完全放弃原子化工具。我后来找到了合理的分工:
| 样式类型 | 使用方式 | 理由 |
|---|---|---|
| 布局、间距(通用)、定位、flex/grid | UnoCSS 原子类(flex, p-4, gap-2) | 无需映射,开箱即用,书写快 |
| 颜色、主题间距、圆角、阴影、动效 | 组件 scoped 样式 + var(--ui-*) | 语义化、可维护、无映射成本 |
这里有一个重要的边界:响应式设计。原子化CSS的 p-sm md:p-md 写法确实比在 scoped 样式里写媒体查询更简洁。对于复杂的响应式布局,我仍然会使用 UnoCSS 的响应式变体,但仅限于与设计令牌无关的通用属性(如 flex、grid、padding 的数值直接用 UnoCSS 的默认比例,不强行映射我的令牌)。这算是一种务实的妥协。
升华:DTCG 与设计令牌的行业趋势
值得一提的是,W3C 旗下的设计令牌社区组(DTCG)已在 2025 年底发布首个稳定规范,推动设计令牌成为跨平台通用语言。这意味着“以令牌为中心”的架构,正成为行业共识。
原子化工具可以消费设计令牌,但不应绑架设计令牌。将令牌硬编码成特定工具的原子类,相当于把自己的设计系统锁死在该工具的语法上。而直接使用 CSS 变量,则是框架无关、面向未来的选择。
融合之道:给同样处境的开发者建议
如果你和我一样,已经有一套完整的设计令牌系统(CSS变量、Design Tokens),但想尝试原子化CSS,我的建议是:
- 不要映射颜色、主题间距、圆角等核心令牌。这些应该直接通过
var(--ui-*)在 scoped 样式中使用。 - 只使用原子化工具提供的通用布局类(
flex,p-4,gap-2,relative……)。这些与你的设计系统无关,且无需任何映射。 - 警惕“魔法数字”:避免在模板里写
p-3.5或gap-11这种硬编码值,它们会破坏设计令牌的契约。如果你发现自己经常需要偏离令牌系统,说明你的令牌定义可能不够灵活,需要扩展而非绕过。 - 对于响应式布局,可以继续使用原子化工具的响应式变体(
md:p-4),但尽量只用于通用属性,不用于颜色/主题。 - 如果原子化工具让你觉得“为了用而用”,完全可以不用。原生CSS + 设计令牌已经足够清晰、可维护。
成本/收益对比
为了更直观地展示两种方案的差异,我画了一个简表:
| 维度 | 映射方案 (原子类+令牌) | 融合方案 (scoped样式+令牌+布局原子类) |
|---|---|---|
| 书写速度 | 极快(全在模板) | 中等(CSS + 模板切换) |
| 维护成本 | 高(双向同步配置) | 低(单一真实来源) |
| 语义清晰度 | 差(bg-bg-muted) | 优(var(--ui-bg-muted)) |
| 响应式便捷度 | 高(md:p-4) | 中(需写媒体查询) |
| 组件库分发 | 强绑定工具 | 框架无关,仅依赖CSS |
| 调试体验 | 差(需查映射) | 好(直接看到变量) |
对于个人项目,维护成本、语义清晰度、分发兼容性往往比极致的书写速度更重要。因此融合方案胜出。
结论:设计令牌优先,原子化可选
UnoCSS 和 Tailwind 是好工具,但它们不是设计系统的替代品。设计令牌才是地基,原子化只是上面的一层涂料。当你已经有一块坚实的地基时,是否涂上这层涂料,取决于你愿不愿意接受那点语法糖带来的维护成本。
至少对我来说,不划算。
最终,我的 Button.vue 回到了最原始的样子:
<style scoped>
.mg-button {
background-color: var(--ui-primary);
color: white;
padding: var(--ui-spacing-md) var(--ui-spacing-lg);
}
</style>
简单、直接、语义化。这才是设计令牌该有的用法。
如果这篇文档对你有帮助,可以请我喝杯咖啡 ☕️

