Design Token 第14周笔记-把动态 palette 升级成语义 CSS variable token 系统
路线图阶段:第三阶段(Design Engineer 工具箱)第14周 —— Design tokens 系统化 任务:把”组件里到处 hslToCss(palette.xxx) 运行时算颜色”升级成”一套语义 CSS variable token,组件引用 var(—color-xxx)” 核心收获:CSS variable(机制)≠ design token(设计意图);组件和数据解耦 = shadcn 形态;token 的边界(消费者 vs 展示器)
⚠️ 守住的 scope:这周只做语义 CSS variable token。没碰 Style Dictionary、multi-theme、dark mode —— 那些是进阶,做组件库大作业时再上。
0. 概念:什么是 design token
design token(设计令牌)= 给一个设计决策起一个有名字、可引用的变量。
不再到处写死具体值(#C16952、16px),而是定义有语义名字的变量(--color-primary),所有地方引用它。改一处,全局变。
Figma variable/style ↔ Design token
color/primary = #C16952 --color-primary: #C16952
spacing/md = 16px ↔ --spacing-md: 16px
你在 Figma 里每天都在做:不在每个图层填
#C16952,而是引用color/primary。design token 就是把这套搬进代码 —— 设计和代码之间共享的同一套词汇表(shared vocabulary)。
1. ⭐⭐ 关键区分:CSS variable ≠ design token
CSS variable(技术机制) design token(设计概念)
= 浏览器提供的语法工具 = 有语义、可复用、代表设计决策的命名值
= "我能存个值在变量里" = "这个值代表 primary 角色,全系统统一引用"
- CSS variable 是底层工具——能装任何值(颜色/尺寸/临时中间值)。
- design token 是上层概念——经过设计、有语义名、系统级、多处复用的值。
token ⊂ CSS variable(token 是 CSS variable 的一个子集)。 所有 web design token 都用 CSS variable 实现,但不是所有 CSS variable 都是 token。
week11 的 --hover-color 算 token 吗?——不算
--hover-color(week11) | --color-primary(本周) | |
|---|---|---|
| 是 CSS variable | 是 | 是 |
| 有系统级语义 | 否(只是 secondary 临时别名) | 是(代表 primary 角色) |
| 多处复用 | 否(一个 hover) | 是(按钮/边框/标题都引用) |
| 算 design token | 否 | 是 |
week11 让你学会了机制;本周把这个机制用来表达 design system 决策 → 才升级成 token。 面试能讲清这个区别会显得有深度:token 的价值在语义和系统性,不在
--语法。
2. ⭐ semantic naming(语义命名)—— token 的灵魂
❌ 按"长什么样"命名 ✅ 按"是什么用途"命名
--color-orange-500: #C16952 --color-primary: #C16952
场景:产品改版,主色橙 → 蓝。
--color-orange-500→ 换蓝色后名字还叫 orange → 名实不符(name-reality mismatch),灾难。--color-primary→ 换什么颜色都叫 primary → 语义不变,只改值,引用处一行不动。
这是 week11 角色映射(primary=行动点、accent=点睛)的代码化。 token 命名描述”这个颜色扮演什么角色”,不是”它是什么颜色”。
3. 核心难点:颜色是 runtime 动态的
普通教程 token 写死在 CSS:
:root { --color-primary: #C16952; } /* 静态 */
但你的颜色是运行时算的(用户选主色 → generatePalette → 五个角色),写代码时不知道值。 → 不能写静态 CSS,得运行时把算好的 palette 注入成一组 CSS variable。
这是 week11
--hover-color那招的扩展版:那次注入一个变量,这次注入一整套。
4. 实现三步
第一步:paletteToCssVars —— palette 转成 CSS variable 对象
// colors.ts(逻辑层)
export function paletteToCssVars(palette: Palette) {
const vars = Object.entries(palette).reduce(
(acc, [role, color]) => {
acc[`--color-${role}`] = hslToCss(color); // key 加 --color- 前缀,value 转字符串
return acc;
},
{} as Record<string, string>, // 汇总成对象 → 初始值 {}
);
vars["--color-on-primary"] = textColorFor(palette.primary); // 额外加 on-primary(算出来的)
return vars; // ⚠️ 先存变量再加工 → 必须手动 return!
}
要点:
Object.entries + reduce把对象每项加工、汇总成新对象(week7 “分组计数”同款骨架)。- key 用模板字符串
`--color-${role}`;value 用hslToCss(color)(CSS 只认字符串,HSL 对象要转)。 Record<string, string>= TS 内置工具类型,“key 和 value 都是 string 的对象”。认识即可、照抄(week12 样板 vs 概念)。- ⚠️
return vars别漏:改成”先存变量再加工”后,reduce 不再直接被 return,必须手动 return。 这个坑 TS 不一定报错(推断成 void),但运行时 cssVars = undefined → 页面崩。隐蔽,养成检查 return 的习惯。
第二步:注入容器 —— CSS variable 的 scope(作用域)
// ColorPalette
const cssVars = paletteToCssVars(palette);
// ...
<div className="max-w-5xl ..." style={cssVars as React.CSSProperties}>
{/* 容器内所有后代都能用 var(--color-xxx) */}
CSS variable 在哪个元素定义,就在该元素及其所有后代里生效。 类比 Figma:在一个 frame 上绑定 local variables,frame 内所有图层都能引用。 注入到哪?Swatch 和 PreviewCard 的最近共同父容器(week5 状态提升同款思路:共享的东西放最近共同父)。 验证:DevTools 选中容器,styles 面板能看到那几个
--color-*挂着。
第三步:组件消费 token
backgroundColor: hslToCss(palette.background) // ❌ 旧:运行时各自算
backgroundColor: "var(--color-background)" // ✅ 新:引用容器里的 token
5. ⭐⭐ token 让组件和数据解耦(= shadcn 形态)
PreviewCard 全改成 var(--color-xxx) 后,不再需要 palette prop:
改造前:<PreviewCard palette={palette} /> 组件被喂整包数据,内部 palette.primary 层层取
改造后:<PreviewCard /> 零 props,颜色靠 CSS variable scope 自动"渗透"进来
最终 PreviewCard:零 props、零 import,纯靠 var(--color-xxx) 从容器 scope 拿颜色。
这就是 shadcn / Radix 库组件的形态。组件不知道具体颜色,只声明”我用 primary 角色”,值由外层容器注入。 为什么大型组件库都用 token 而非 props 传颜色:省掉了数据层层传递的负担,scope 自动渗透。
6. on-color 也是 token(on-primary)
按钮底是 primary(深),文字不能也用深色 → 需要”压在 primary 上的对比色” = on-primary。
- 真实 design system 里
--color-primary配--color-on-primary,成对出现。 - value 用现成的
textColorFor(palette.primary)(按亮度返回黑/白字)算好,注入成 token。 - 按钮:
color: "var(--color-on-primary)"。
⚠️ 按需添加:这周只用到 on-primary 就只加它,别一次造五个 on-color(over-engineering)。
7. token 引用 token(aliasing)
"--hover-color": "var(--color-secondary)" // 一个 CSS variable 引用另一个
week11 的 --hover-color 原本值是 hslToCss(palette.secondary)(运行时算);现在直接指向 --color-secondary token。
好处:secondary 变了,
--hover-color自动跟着变(引用 token,不是写死的算出值)。 这叫 token aliasing —— week11 那个”临时变量”真正升级成 token 体系的一员。
8. ⭐⭐ token 的边界:消费者用 token,展示器用 prop
不是所有组件都该改成 token。Swatch 就不该改。
| PreviewCard | Swatch | |
|---|---|---|
| 消费哪个颜色 | 固定角色(背景永远 background、按钮永远 primary) | 动态(这次 primary、下次 accent,由 .map() 循环决定) |
能写死 var(--color-primary) | 能(它就要 primary) | 不能(它要”当前这一项”的颜色,不是固定角色) |
| 用什么 | token(var(--color-xxx)) | prop(color + hslToCss(color)) |
硬把 Swatch 改 token → 五个色块全变成 primary 色(不再跟循环变量走)→ 错。 判断直觉(重要):
- 消费固定语义角色的组件(按钮永远 primary)→ 用 token。
- 动态展示任意颜色的组件(色卡这次显示这个、下次那个)→ 颜色是数据,走 prop。 换说法:PreviewCard 问”primary 现在是什么颜色?”(消费者);Swatch 说”把你给我的颜色画出来”(展示器)。 消费者用 token,展示器用 prop。
第14周验收
- 懂 design token 概念(给设计决策起可引用的语义名)
- ⭐⭐ 分清 CSS variable(机制) vs design token(设计意图),token ⊂ CSS variable
- 懂 semantic naming:
--color-primary不是--color-orange(防 name-reality mismatch) - 写 paletteToCssVars:Object.entries+reduce 转 CSS variable 对象,记得 return
- 懂 CSS variable 的 scope,注入到最近共同父容器
- PreviewCard 改成零 props 消费 token(= shadcn 形态,组件数据解耦)
- 懂 on-color 也是 token(on-primary 成对配色,按需添加)
- 懂 token aliasing(token 引用 token)
- ⭐⭐ 判断 token 边界:消费者用 token、展示器(Swatch)用 prop
反复提醒过、需继续巩固的点
- “先存变量再加工”的函数,最后必须 return(TS 可能不报错,运行时才崩)。
- CSS 只认字符串值:HSL 对象要
hslToCss转换才能进 CSS variable。 - 自定义属性(
--xxx)进 style 时 TS 标红 →as React.CSSProperties(week11 同款坑)。 - 不是所有组件都该 token 化:分清”消费固定角色”(token) vs “动态展示”(prop)。