暗色模式实现模式
基于令牌的主题、闪烁预防与系统偏好检测,构建稳健暗色模式。
组件模式进阶
为什么
用户期望暗色模式 — 30%+ 系统级启用。差的实现加载时白闪、忽略 OS 偏好或硬编码颜色破坏品牌主题。令牌驱动无闪烁水合是基本要求。
何时使用
任何面向消费者的应用、文档站或夜间使用工具都应实现暗色模式。纯打印或固定照明的嵌入式自助终端 UI 跳过。
如何做
1. 定义令牌对 — --bg-light / --bg-dark,绝不组件特定暗色覆盖
2. 设置 CSS — :root { --bg: #fff } :root[data-theme="dark"] { --bg: #0f172a }
3. 内联脚本 — 首次绘制前读 localStorage + matchMedia 设置 data-theme
4. 切换组件 — 日/月图标,更新属性 + localStorage,派发事件
5. 系统同步 — matchMedia("(prefers-color-scheme: dark)") 监听器,用户选 "system" 时更新
6. 图片 — 通过 <picture> 或 CSS filter 提供暗色优化变体
7. 测试 — Storybook 中切换,Playwright CI 截图两主题
示例
<!-- index.html — <body> 前内联脚本防闪烁 -->
<head>
<script>
(function () {
var t = localStorage.getItem("theme") || "system";
var dark = t === "dark" || (t === "system" &&
matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.dataset.theme = dark ? "dark" : "light";
})();
</script>
</head>
/* tokens.css — 成对令牌,组件仅引用 --bg */
:root { --bg: #ffffff; --text: #0f172a; --surface: #f1f5f9; }
:root[data-theme="dark"] { --bg: #0f172a; --text: #f1f5f9; --surface: #1e293b; }
body { background: var(--bg); color: var(--text); }
// ThemeToggle.tsx — 更新属性 + 存储 + 通知
function ThemeToggle() {
const [theme, setTheme] = useState(localStorage.getItem("theme") || "system");
const apply = (t: string) => {
localStorage.setItem("theme", t);
document.documentElement.dataset.theme =
t === "dark" || (t === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
? "dark" : "light";
setTheme(t);
};
return <button onClick={() => apply(theme === "dark" ? "light" : "dark")}>
{theme === "dark" ? "☀" : "☾"}
</button>;
}
常见问题
- ⚠FOUC(无样式内容闪烁)— 主题在水合后应用,刷新时白闪
- ⚠硬编码暗色 — .dark .btn { background: #1e293b } 而非令牌,破坏白标
- ⚠遗忘表面 — bg 令牌换了但模态/输入仍是浅表面,看起来坏了
- ⚠Logo 不可见 — 黑 logo 在暗背景上,未提供 <picture> 变体
最佳实践
- ✓在 <head> CSS 前内联主题脚本 — 零闪烁,不依赖水合
- ✓用 3 模式:light / dark / system — 多数应用强制二选一忽略 OS 偏好
- ✓审计暗色下每个表面 — 用 Storybook dark addon,非手动切换
- ✓通过 <picture><source media='(prefers-color-scheme: dark)'> 提供暗色优化 logo
常见问题解答
如何防止页面加载白闪?
在 <head> CSS 前内联脚本读 localStorage 和 matchMedia,再在 <html> 设 data-theme。首次绘制前应用主题,无闪烁。
应支持 light/dark 还是也支持“system”?
三模式:light、dark、system。“system”跟随 prefers-color-scheme 并随 OS 变化更新。多数应用强制二选一忽略 OS 偏好,用户不喜欢。
为什么暗色下 logo 不可见?
黑 logo 在暗背景消失。通过 <picture><source media='(prefers-color-scheme: dark)'> 提供暗色变体,或用 CSS filter 反相。
如何主题化而不每组件硬编码暗色?
定义成对令牌(--bg、--text、--surface)在 :root[data-theme='dark'] 覆盖。组件仅引用 var(--bg)——绝不组件特定暗色覆盖。
如何审计暗色下每个表面?
用 Storybook dark-mode addon 切换所有 stories,Playwright CI 截图两主题。别手动点——会漏模态和输入。