D03 暗色主题与设计令牌
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 验证与讨论
- 点击切换按钮,确认整页配色变化、组件无遗漏。
- 刷新页面,确认主题保持且首屏无闪白。
- 清除存储后刷新,确认跟随系统偏好。
- 用取色器复核暗色下正文对比度不低于 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 |
- 上一篇:D02-响应式文章列表
- 返回:D00-总结 | 科目 README
参考文献
[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
还没有评论,快来抢沙发吧~