CSS三层Token:比Sass变量更强的样式管理方案
最近看到一个设计系统,用原生CSS的三层Token管理样式。看完之后我的判断是:在抽象能力上,这套系统比Sass变量强。
听起来有点反直觉。预处理器应该更强才对,有更多语法糖,更多编程能力。但三层Token的分层设计,让主题切换和组件复用变干净了,这是Sass做不到的。
Sass变量写久了会乱
项目小的时候,几个变量够用。$primary-color、$text-color,清清楚楚。项目一大,变量开始失控。$primary-dark、$primary-light、$text-primary、$text-secondary……谁也记不住哪个该用在哪儿。
加暗黑模式更烦。得新建一套变量:$dark-primary、$dark-text……然后代码里到处@if $theme == dark。改完暗黑模式,下次加护眼模式,又来一遍。
还有一个问题:语义和数值绑死了。$primary-color: #3B82F6,“主色”和“蓝色”绑在一起。想换颜色,得改变量值。但“主色”在不同组件里可能不一样。按钮的主色、链接的主色……拆着拆着变量越来越多。
这些问题的根源:Sass变量只有一层,没有分层抽象。
三层Token怎么分层
三层Token把变量分成三层,每层有明确职责。
Primitives:原始层(--p-)
这一层只定义“有什么”,不管“用在哪儿”。
/* 颜色 */
--p-color-blue-500: #3B82F6;
--p-color-blue-600: #2563EB;
--p-color-gray-100: #F3F4F6;
--p-color-gray-900: #111827;
/* 间距 */
--p-spacing-1: 0.25rem;
--p-spacing-2: 0.5rem;
--p-spacing-4: 1rem;
/* 圆角 */
--p-radius-sm: 0.25rem;
--p-radius-md: 0.5rem;这些变量不带语义。--p-color-blue-500只是一个蓝色,不代表“主色”或“链接色”。语义在下一层定义。
Semantic:语义层(--s-)
给Primitives赋予含义。这一层决定“用在哪儿”。
/* 语义定义 */
--s-color-primary: var(--p-color-blue-500);
--s-color-primary-hover: var(--p-color-blue-600);
--s-color-text: var(--p-color-gray-900);
--s-color-text-muted: var(--p-color-gray-100);
--s-spacing-default: var(--p-spacing-4);组件只读Semantic,不直接读Primitives。这样的好处是:换主题时只改Semantic,不动组件代码。
Component:组件层(--c-)
组件专用的覆盖钩子。
.button {
--btn-bg: var(--c-button-bg, var(--s-color-primary));
--btn-radius: var(--c-button-radius, var(--p-radius-md));
background: var(--btn-bg);
border-radius: var(--btn-radius);
}var(--c-button-bg, var(--s-color-primary))的意思是:如果定义了组件级覆盖就用它,否则回退到语义默认值。
某个按钮想用绿色?只改Component层:
.button--success {
--c-button-bg: var(--p-color-green-500);
}不动Semantic,不影响其他按钮。
为什么比Sass变量强
主题切换只改一层
加暗黑模式,只改Semantic:
:root {
--s-color-text: light-dark(
var(--p-color-warm-text), /* 亮色模式 */
var(--p-color-cool-text) /* 暗黑模式 */
);
--s-color-bg: light-dark(
var(--p-color-warm-bg),
var(--p-color-cool-bg)
);
}Primitives不动,Component不动,只改Semantic的映射关系。Sass要改主题?新建整套变量,然后代码里到处判断。
切换机制也很简单:
html { color-scheme: light dark; } /* 跟随系统 */
html.light { color-scheme: light only; } /* 强制亮色 */
html.dark { color-scheme: dark only; } /* 强制暗色 */JS只负责切换.light或.dark类,不用改任何CSS变量。
配合原生函数
light-dark()是CSS原生函数,一个Token搞定两种主题。Sass没这个能力,得自己写mixin:
// Sass的做法
@mixin themed {
@content;
.dark & {
@content($theme: dark);
}
}然后每个用到颜色的地方都包一层。三层Token直接在变量定义里解决,组件代码不用动。
运行时能力
CSS变量是运行时的,JS可以动态改:
// 用户选了自定义主题色
document.documentElement.style.setProperty(
'--s-color-primary',
userPickedColor
);Sass变量编译完就固定了。用户运行时切换主题?得重新加载CSS或用JS替换类名,没有CSS变量干净。
命名不失控
--p-、--s-、--c-,三层命名有规则。一看变量名就知道:
--p-color-blue-500:原始蓝色,定义色板时用
--s-color-primary:主色,组件里用
--c-button-bg:按钮背景覆盖,特定按钮用
Sass变量只有一层,命名全靠自觉。项目小还好,项目大了各人命名习惯不一样,变量名越来越乱。
什么时候该用三层Token
三层Token确实能把主题切换做得很干净,但这种干净是用前期的约束换来的。
如果你的业务只是一些三五页面的轻量项目,或者只需要偶尔切个暗黑模式,那强推这套分层就是过度设计。更别说如果是重度依赖Sass的老项目,光剥离各种mixin和function就能脱层皮。对于这种轻量或历史负担重的场景,老老实实用单层变量配合light-dark()函数兜底就行:
:root {
--color-text: light-dark(#111827, #F9FAFB);
--color-bg: light-dark(#FFFFFF, #111827);
}这套分层架构真正能发挥威力的场景,是那些变量规模快要失控的中大型前端工程,或是需要跨产品集成的正式设计系统。在这些场景里,团队的痛点早就不再是多写几行代码,而是“新人一上手,根本不敢改以前的颜色”。这个时候,靠着一套严谨的前缀规范(--p-、--s-、--c-)和强隔离的语义映射,你的样式架构才能真正立得稳。
总结
原生CSS的Token系统,强在分层抽象。
Sass变量只有一层,变量多了命名失控,主题切换到处改。三层Token把变量分成Primitives、Semantic、Component,每层职责清晰。配合light-dark()和运行时能力,主题切换和组件复用都做得更干净。
但有成本。小项目、Sass重度依赖、主题简单,别硬上。
不是“应该用什么”,而是“什么场景用什么”。
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!