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(设计令牌)= 给一个设计决策起一个有名字、可引用的变量。

不再到处写死具体值(#C1695216px),而是定义有语义名字的变量(--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 角色,全系统统一引用"

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

场景:产品改版,主色橙 → 蓝。

这是 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!
}

要点:

第二步:注入容器 —— 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

⚠️ 按需添加:这周只用到 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 就不该改。

PreviewCardSwatch
消费哪个颜色固定角色(背景永远 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周验收


反复提醒过、需继续巩固的点