--color-primary 是最容易被滥用的变量名。它的问题不是不好用,而是它把决策和取值绑死了。
# 三层结构
一个能长期维护的 token 体系,通常有三层:
# 1. 原始值(primitive)
只描述"这是什么颜色",不带语义。
--color-violet-500: #7c5cff;
--color-cyan-400: #22d3ee;
--color-slate-950: #0a0b10;
这一层不直接被组件使用。
# 2. 语义层(semantic)
描述"用在哪里"。
--surface-base: var(--color-slate-950);
--surface-raised: #12141c;
--text-primary: #eef1f8;
--text-muted: #98a2b8;
--accent: var(--color-violet-500);
--border-subtle: rgba(255,255,255,.08);
# 3. 组件层(component)
只在必要时存在,用来说明"这个组件怎么用语义 token"。
--card-bg: var(--surface-raised);
--card-border: var(--border-subtle);
--card-radius: 16px;
# 为什么不能只有一层
假设你只有 --color-primary。有三处需要"稍微淡一点的边框",你怎么命名?
--color-primary-10?--border-light?--color-gray-800?
没有语义层,命名就会开始描述程度而不是用途,很快就没人知道该用哪个了。
而有了 --border-subtle,深浅调整只发生在一个地方。
# 暗色主题不是反色
换肤最常见的错误做法,是把所有颜色取反。结果通常是:纯黑背景 + 纯白文字,对比度过高,长时间阅读眼睛疼。
正确的做法是在语义层做映射:
:root {
--surface-base: #f7f7f9;
--text-primary: #14161c;
--text-muted: #5b6472;
--accent: #6d4aff;
}
[data-theme='dark'] {
--surface-base: #0a0b10; /* 不是 #000 */
--text-primary: #eef1f8; /* 不是 #fff */
--text-muted: #98a2b8;
--accent: #8b74ff; /* 暗底上提亮,保证对比度 */
}
组件完全不感知主题,只引用语义 token。
# 增量落地
不要为了 token 化停下来两周做重构。可以这样切:
- 把现有硬编码颜色按出现频次排序,取前 20 个
- 为它们建立语义名,先在旧代码里引入变量但保持取值不变
- 新代码强制走 token,旧代码在改动时顺手迁移
- 加一条 lint 规则:组件 CSS 里出现十六进制颜色就警告
半年之后,你会发现换主题只需要改二十行。