D03 暗色主题与设计令牌

2026-10-31 16:590 阅读约 11 分钟CSS

D03 暗色主题与设计令牌

摘要:暗色主题不是把颜色取反,而是重新定义一套语义:背景变深、文字变浅、品牌色提亮、阴影失效改由边框承担。可控的做法是三层令牌——基础色阶、语义角色、组件局部变量——切换主题只改语义层。本文给出令牌文件、切换方式、无闪烁加载与对比度复核四项内容。
关键词:暗色主题,设计令牌,语义层,切换,无闪烁

所属:frontend/css / D-实战 | 上一篇:D02-响应式文章列表 | 下一篇:无 | 返回:D00-总结

1 引言

主题切换失败的常见表现有三种:切换后部分文字看不见、首屏闪白再变黑、组件在两种主题下样式不一致。三者都源于令牌层级混乱。

本文给出三层令牌与切换实现。范围见表 1-1。

项 本文覆盖 本文不覆盖
令牌 基础色阶、语义角色、组件变量 品牌视觉设计
切换 data-theme、系统偏好、持久化 服务端主题注入
质量 对比度复核、无闪烁加载、过渡 打印样式

2 预备知识

项 要求 核对方式
环境 本地服务器可用 浏览器能打开 localhost
前置 B06 变量与主题基础
前置 C04 对比度与视觉要求
文件 css-practice/tokens.css 与 theme.js 编码 UTF-8

3 原理与实现

3.1 三层令牌

三个层级各自负责不同变化频率,混合后主题切换就会失控,如图 3-1 所示。

flowchart LR
    A["基础层<br/>--blue-600 / --gray-900<br/>极少改"] --> B["语义层<br/>--surface / --text / --brand<br/>按主题赋值"]
    B --> C["组件层<br/>--btn-bg / --card-border<br/>按组件赋值"]
    C --> D["样式规则<br/>只引用组件或语义变量"]
    E["[data-theme=dark]"] --> B

图 3-1 三层令牌与主题赋值位置

图上可见:主题只改语义层;组件层若直接写基础色阶,暗色下必须逐组件修改。

3.2 令牌与切换

/* 运行环境:Chrome 130+;css-practice/tokens.css */
:root {
  --blue-600: #2563eb;
  --blue-400: #60a5fa;
  --gray-900: #0f172a;
  --gray-100: #f1f5f9;
  --gray-0: #ffffff;

  --surface: var(--gray-0);
  --surface-alt: var(--gray-100);
  --text: var(--gray-900);
  --text-muted: #475569;
  --border: #e2e8f0;
  --brand: var(--blue-600);
  --shadow: 0 8px 24px rgb(15 23 42 / 0.12);
}

[data-theme="dark"] {
  --surface: #0b1220;
  --surface-alt: #131c2e;
  --text: #e2e8f0;
  --text-muted: #94a3b8;
  --border: #263349;
  --brand: var(--blue-400);
  --shadow: none;
}

body { background: var(--surface); color: var(--text); transition: background-color 200ms ease; }

.card {
  --card-bg: var(--surface-alt);
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 0.75rem;
  padding: 1.25rem;
  box-shadow: var(--shadow);
}
// 运行环境:浏览器控制台或 theme.js;切换并持久化
const root = document.documentElement;
const saved = localStorage.getItem('theme');
if (saved) root.dataset.theme = saved;

document.getElementById('theme-toggle').addEventListener('click', () => {
  const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
  root.dataset.theme = next;
  localStorage.setItem('theme', next);
});

令牌层级与职责见表 3-1。

层级 示例 何时修改
基础 --blue-600、--gray-900 品牌色改版
语义 --surface、--text、--brand 主题切换、视觉调整
组件 --card-bg、--btn-bg 组件迭代

暗色主题的四项调整见表 3-2。

项 通用做法 原因
背景 用深蓝灰而非纯黑 纯黑与白字对比过强
文字 用近白而非纯白 纯白在深色上易产生光晕
品牌色 提亮一档 深底上原色对比不足
阴影 取消或改边框 深色下阴影不可见

无闪烁加载的做法:在 <head> 内内联防闪脚本,先于样式计算设置主题属性。

<script>
  const t = localStorage.getItem('theme');
  if (t) document.documentElement.dataset.theme = t;
</script>

4 验证与讨论

  1. 点击切换按钮,确认整页配色变化、组件无遗漏。
  2. 刷新页面,确认主题保持且首屏无闪白。
  3. 清除存储后刷新,确认跟随系统偏好。
  4. 用取色器复核暗色下正文对比度不低于 4.5:1。
序号 验收项 判定标准 依据
1 切换生效 背景、文字、边框、品牌色同步变 表 3-1
2 组件零改动 组件样式文件未修改 表 3-1
3 无闪白 首屏直接呈现目标主题 3.2 节脚本
4 持久化 刷新后保持 3.2 节脚本
5 跟随系统 未设置时按系统偏好 图 3-1
6 对比度 暗色正文 ≥ 4.5:1 表 3-2

五项取舍见表 4-1。

决策点 采用方案 备选方案 不选的理由
主题标识 data-theme 属性 切换根类名 属性选择器语义清晰,便于脚本读写
切换范围 语义层整体赋值 逐组件覆盖 组件覆盖在新增组件时必漏
首屏 head 内联防闪脚本 样式加载后再设置 后者必然出现闪白
阴影 暗色下改边框 加深阴影 深底上阴影几乎不可见
过渡 仅背景色过渡 全属性过渡 全属性会连带文字颜色产生闪烁

讨论与局限。 内联脚本与内容安全策略可能冲突,需要在策略中放行或改用 nonce。另系统偏好变化时页面不会自动跟随,除非监听 matchMedia 变化。

5 常见问题

现象 原因 处理
切换后部分文字消失 组件写死浅色背景/深色文字 全部改用语义变量
刷新闪白 主题在样式加载后才设置 用 head 内联脚本
暗色下边框看不见 边框色未随主题变化 增加 --border 语义变量
按钮在暗色下对比不足 沿用原品牌色 提亮一档
切换后滚动条样式不变 未设置 color-scheme 在根元素加 color-scheme: light dark

6 结论

序号 结论 依据
1 主题只改语义层 图 3-1
2 暗色不是反色,需重选背景与品牌色 表 3-2
3 防闪白靠 head 内联脚本 3.2 节脚本
4 切换后必须复核对比度 4 节步骤 4

参考文献

[1] MDN Web Docs · color-scheme,https://developer.mozilla.org/zh-CN/docs/Web/CSS/color-scheme(访问于 2026-10-31)
[2] web.dev · 深色模式设计,https://web.dev/articles/prefers-color-scheme(访问于 2026-10-31)

返回文章列表

评论 0

0/2000

还没有评论,快来抢沙发吧~