文章工具与开发

设计 token 的三层结构:让多主题不再是第二套样式

深色模式之所以难维护,往往是因为颜色直接写在了组件里。把 token 分成原始值、语义与组件三层,主题切换就只是换一组映射。

米白色纸面上的几何线条与一枚绿色圆点(示意插图)
示意插图 · Waylan

给一个已有的站点加深色模式,最常见的做法是复制一份样式,再逐个覆盖颜色。这种做法第一周很快,之后每加一个组件都要维护两份规则,迟早会有遗漏。问题的根源在于:组件直接引用了具体的颜色值,而不是引用颜色的用途。

三层 token

我现在把设计 token 分成三层,每一层只引用上一层:

  1. 原始值(primitive):调色板本身,例如 --green-600、--sand-50。它们只描述“是什么颜色”,不描述用途,也不随主题变化。
  2. 语义(semantic):描述用途,例如 --color-text、--color-surface、--color-accent。主题切换只发生在这一层。
  3. 组件(component):只在确有必要时出现,例如 --button-bg。它引用语义 token,让组件可以局部调整而不破坏全局约定。

组件样式只允许使用语义或组件 token。代码评审时,一旦看到组件里出现原始色值或十六进制颜色,就说明分层被绕过了。

用 CSS 自定义属性实现

CSS 自定义属性会沿 DOM 继承,并且可以在运行时改变,天然适合承载语义层:

css
:root {
  --green-600: #3f6b4f;
  --green-300: #8fb59a;
  --sand-50: #faf8f3;
  --ink-900: #1d1f1c;
  --ink-100: #e8e6df;

  --color-surface: var(--sand-50);
  --color-text: var(--ink-900);
  --color-accent: var(--green-600);
  color-scheme: light;
}

:root[data-theme="dark"] {
  --color-surface: var(--ink-900);
  --color-text: var(--ink-100);
  --color-accent: var(--green-300);
  color-scheme: dark;
}

.button-primary {
  background: var(--color-accent);
  color: var(--color-surface);
}

color-scheme 值得单独一提:它告诉浏览器当前页面的配色倾向,表单控件、滚动条这类由浏览器绘制的部分会随之切换,不需要我们逐个覆盖。

避免首屏闪烁

如果主题偏好存放在 localStorage,而读取发生在应用脚本里,深色模式的读者会先看到一帧浅色页面。解决办法是在 <head> 中放一段很小的同步脚本,在首次绘制之前写好 data-theme:

js
(() => {
  let stored = null;
  try {
    stored = localStorage.getItem("theme");
  } catch {
    // 某些隐私模式或 file:// 环境下不可用,退回系统偏好
  }
  const dark = stored
    ? stored === "dark"
    : matchMedia("(prefers-color-scheme: dark)").matches;
  document.documentElement.dataset.theme = dark ? "dark" : "light";
})();

这段脚本会阻塞解析,所以要保持足够短,并且不依赖任何模块。对静态预渲染的站点来说,它还意味着 <html> 上的属性会在水合前被修改,需要确认框架不会因此报告不一致。

主题不是第二套样式,而是同一套语义的另一组取值。

检查清单

检查项 做法
组件不出现原始色值 用 Stylelint 规则或简单的 grep 在 CI 中拦截
两套主题的对比度 对语义 token 组合逐对检查,而不是逐个页面检查
浏览器绘制的控件 通过 color-scheme 跟随主题
首屏主题 同步脚本在首次绘制前写入 data-theme

分层的收益在第三个主题出现时最明显:高对比度主题只需要再定义一组语义映射,组件代码一行都不用改。相关的动效偏好处理,见读取“减少动态效果”偏好。

参考:MDN · 使用 CSS 自定义属性 · MDN · prefers-color-scheme · MDN · color-scheme · W3C Design Tokens Community Group

读到这里,
感谢你留下这一小段时间。

设计系统CSS主题

发表评论…

公开讨论尚未连接

关于整篇文章,也关于让你停下来的那句话。
公开评论基于 GitHub Discussions;接入完成后才可发表。

正在加载评论…
发表评论
支持代码块与表情 · 评论公开可见0 / 6000

正在确认你的身份…

放大图片
搜索

输入关键词搜索标题、正文和标签。