实现深色模式:开发者完整指南
Embed This Widget
Add the script tag and a data attribute to embed this widget.
Embed via iframe for maximum compatibility.
<iframe src="https://colorfyi.com/iframe/entity//" width="420" height="400" frameborder="0" style="border:0;border-radius:10px;max-width:100%" loading="lazy"></iframe>
Paste this URL in WordPress, Medium, or any oEmbed-compatible platform.
https://colorfyi.com/entity//
Add a dynamic SVG badge to your README or docs.
[](https://colorfyi.com/entity//)
Use the native HTML custom element.
深色模式已从小众偏好演变为用户的预期功能。macOS、Windows、Android、iOS 等各平台的用户都可以设置全系统的深色外观,他们希望网站和应用程序能够响应这一设置。正确实现深色模式不仅仅是将白色换成黑色——它需要对颜色、对比度和用户控制采取系统化的方法。本指南将完整介绍整个实现过程,从 CSS 架构、JavaScript 切换机制,到对两套主题的全面测试。
CSS 自定义属性主题方案
在 CSS 中处理深色模式,最易维护的方式是使用自定义属性(即 CSS 变量)。不要将颜色值分散在样式表各处,而是将所有颜色定义为 :root 上的变量,然后为深色模式重新定义这些变量。组件样式只引用变量——永远不要使用原始十六进制代码。
定义浅色和深色调色板
从浅色模式调色板作为默认值开始。一个简洁的起点如下:
:root {
/* 背景 */
--color-bg-base: #FFFFFF;
--color-bg-elevated: #F8F9FA;
--color-bg-overlay: #F1F3F5;
/* 文字 */
--color-text-primary: #1A1A2E;
--color-text-secondary: #4A4A6A;
--color-text-muted: #6C757D;
/* 边框 */
--color-border: #DEE2E6;
--color-border-strong: #ADB5BD;
/* 品牌/强调色 */
--color-accent: #3B82F6;
--color-accent-hover: #2563EB;
/* 反馈色 */
--color-success: #22C55E;
--color-warning: #F59E0B;
--color-danger: #EF4444;
}
然后在单独的代码块中定义深色模式的覆盖值。关键在于:你并非简单地反转颜色,而是为深色表面专门选择一套独立的调色板:
[data-theme="dark"] {
/* 背景 */
--color-bg-base: #0F0F17;
--color-bg-elevated: #1A1A2E;
--color-bg-overlay: #252540;
/* 文字 */
--color-text-primary: #E8E8F0;
--color-text-secondary: #A8A8C0;
--color-text-muted: #6A6A88;
/* 边框 */
--color-border: #2E2E4A;
--color-border-strong: #4A4A6A;
/* 品牌/强调色——深色背景上通常需要更浅以保证可读性 */
--color-accent: #60A5FA;
--color-accent-hover: #93C5FD;
/* 反馈色——略微降低饱和度以避免刺眼 */
--color-success: #4ADE80;
--color-warning: #FCD34D;
--color-danger: #F87171;
}
注意,浅色模式的强调色 #3B82F6 在深色模式下变为 #60A5FA。色相相同,但明度提升——这是必要的,因为对比度背景已经翻转。在白色背景上通过 WCAG AA 的颜色,在近黑色背景上几乎必然失败,除非进行调整。色调生成器让你可以探索任意颜色的完整 50–950 范围,便于为每套主题选择合适的色调。
在组件中使用变量
调色板建立后,每个组件只引用变量而不使用原始值:
.card {
background-color: var(--color-bg-elevated);
border: 1px solid var(--color-border);
color: var(--color-text-primary);
}
.btn-primary {
background-color: var(--color-accent);
color: #FFFFFF;
}
.btn-primary:hover {
background-color: var(--color-accent-hover);
}
当 <html> 元素上存在 [data-theme="dark"] 属性时,所有变量同时更新,每个引用它们的组件都会改变外观——无需额外编写任何 CSS。
prefers-color-scheme 媒体查询
在用户与切换按钮交互之前,你可以使用 prefers-color-scheme 媒体查询来响应操作系统偏好。该媒体查询在操作系统设置为深色外观时触发。
@media (prefers-color-scheme: dark) {
:root {
--color-bg-base: #0F0F17;
--color-bg-elevated: #1A1A2E;
--color-bg-overlay: #252540;
--color-text-primary: #E8E8F0;
--color-text-secondary: #A8A8C0;
--color-text-muted: #6A6A88;
--color-border: #2E2E4A;
--color-border-strong: #4A4A6A;
--color-accent: #60A5FA;
--color-accent-hover: #93C5FD;
--color-success: #4ADE80;
--color-warning: #FCD34D;
--color-danger: #F87171;
}
}
这种方式无需 JavaScript,不产生布局偏移,并在页面加载时立即响应用户声明的偏好。这是正确的基准实现。局限在于用户无法在你的应用内覆盖它——如果操作系统是深色,网站就是深色,无从逃脱。这也是为什么大多数生产级实现会在媒体查询之上叠加 JavaScript 切换功能。
两种方式结合使用
推荐模式是将媒体查询作为默认值,将 data-theme 属性作为显式覆盖。可以通过 CSS 特异性技巧或正确的规则顺序来实现:
/* 1. 浅色模式默认值 */
:root {
--color-bg-base: #FFFFFF;
/* ... */
}
/* 2. 操作系统深色模式覆盖(未设置显式偏好时) */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-bg-base: #0F0F17;
/* ... */
}
}
/* 3. 显式深色模式(用户通过 JS 切换) */
[data-theme="dark"] {
--color-bg-base: #0F0F17;
/* ... */
}
媒体查询中的 :not([data-theme="light"]) 选择器意味着,只有在用户没有显式选择浅色模式时,操作系统深色偏好才会生效。一旦用户切换,其显式选择优先。
JavaScript 切换机制
一个实现良好的切换需要完成三件事:立即改变当前外观、将偏好持久化到 localStorage,以及在首次绘制前从 localStorage 读取已保存的偏好。
页面加载时读取偏好
这段脚本必须在 <head> 中运行——在页面渲染之前——以防止页面加载时闪现错误的主题:
<head>
<script>
(function() {
const stored = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = stored ?? (prefersDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
</head>
这段代码在任何样式应用之前立即在 <html> 上设置 data-theme。浏览器从首次绘制起就使用正确的自定义属性值——不会闪烁。
切换函数
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
}
// 绑定到按钮
document.getElementById('theme-toggle').addEventListener('click', toggleTheme);
同步切换按钮状态
切换按钮应在视觉上反映当前模式。一种简单的方法是使用图标:
<button id="theme-toggle" aria-label="切换深色模式">
<span class="icon-light">☀️</span>
<span class="icon-dark">🌙</span>
</button>
[data-theme="dark"] .icon-light { display: none; }
[data-theme="dark"] .icon-dark { display: inline; }
[data-theme="light"] .icon-light { display: inline; }
[data-theme="light"] .icon-dark { display: none; }
由于图标可见性由与 data-theme 关联的 CSS 变量控制,每当属性变化时,按钮状态自动更新——无需额外的 JavaScript。
配色适配策略
选择深色模式颜色并不像反转浅色调色板那么简单。以下几个原则指导良好的深色配色选择。
降低对比度,而非简单翻转
纯白色文字在纯黑色背景上(#FFFFFF 叠加 #000000)技术上是最大对比度 21:1,但对长时间阅读而言认知负担过重。缩小两端的极值:正文使用接近白色的 #E8E8F0,页面背景使用深海军蓝 #0F0F17。这样可以保持充裕的对比度(仍高于 15:1),同时减少视觉疲劳。
使用对比度检查器验证深色主题中每对文字与背景的组合至少满足 WCAG AA(普通文字 4.5:1,大文字 3:1)。常见失败点包括:
- 表单输入框中的占位文字
- 禁用按钮标签
- 次要元数据文字(时间戳、作者信息)
- 无可见标签的仅图标按钮
用深色表面传达层级
在浅色模式下,层级通常通过投影来表达。在深色模式下,阴影在深色背景上几乎不可见。Material Design 3 规范引入了一种更有效的方法:更浅的表面传达更高的层级。为悬浮组件使用微妙的更浅背景:
/* 深色模式层级刻度 */
--color-bg-base: #0F0F17; /* 页面背景 */
--color-bg-elevated: #1A1A2E; /* 卡片、侧边栏 */
--color-bg-overlay: #252540; /* 模态框、下拉菜单 */
--color-bg-tooltip: #2E2E4A; /* 工具提示 */
#0F0F17 作为基础,#1A1A2E 用于卡片,#252540 用于模态框——每一步在 HSL 明度上约提升 8–10%。这样无需依赖阴影即可创建清晰的视觉层级。
深色模式颜色适度降低饱和度
高饱和度颜色在深色背景上看起来刺眼,带有霓虹感。将品牌色适配到深色模式时,在提高明度的同时将饱和度降低 10–20%。与其使用鲜艳的 #22C55E 成功绿,不如选用 #4ADE80——更浅且略微降低饱和度,传达成功的同时避免视觉疲劳。
色调生成器在此处非常有用:输入你的品牌主绿色或蓝色,探索深色模式文字和图标用途的 300–400 范围,以及交互元素的 500–600 范围。
图片和媒体
白色背景的图片在深色模式下显得突兀。CSS 可以提供帮助:
/* 在深色模式下减少图片的刺眼感 */
[data-theme="dark"] img:not([src*=".svg"]) {
filter: brightness(0.9) contrast(1.05);
}
/* 或让图片与背景略微融合 */
[data-theme="dark"] img {
mix-blend-mode: luminosity;
opacity: 0.9;
}
对于需要适配的 SVG 图标,使用 currentColor 作为 fill 值可以让它们自动采用当前文字颜色:
.icon { color: var(--color-text-secondary); }
<svg fill="currentColor" viewBox="0 0 24 24">...</svg>
测试两种模式
全面测试可防止深色模式问题悄悄进入生产环境。
浏览器 DevTools 模拟
Chrome 和 Firefox 都提供 DevTools 深色模式模拟,无需更改操作系统设置。在 Chrome 中:打开 DevTools,点击三点菜单,进入更多工具 → 渲染,将"模拟 CSS 媒体特性 prefers-color-scheme"设置为"dark"。这样可以并排比较两种模式。
自动化对比度测试
人工抽查容易出错。将自动化对比度审计集成到开发工作流程中。在 CI 中使用 Axe 或 Lighthouse 等工具捕获不符合 WCAG 阈值的新颜色。对比度检查器让你能够快速验证任意前景/背景组合与所有 WCAG 等级的关系——粘贴任意十六进制对,立即查看对比度。
用真实内容测试
深色模式问题经常出现在有动态内容的页面上:用户上传的图片、第三方嵌入内容、图表和地图。要针对真实的内容样本进行测试,而非仅测试孤立的组件库。
操作系统级测试
通过 DevTools 模拟验证后,要实际将操作系统设置为深色模式进行测试。prefers-color-scheme 媒体查询基于操作系统设置触发,部分浏览器在真实设置与模拟设置下的行为会略有差异。还要测试切换过渡:在页面打开时切换模式,确认不会发生布局偏移或渲染异常。
常见问题检查清单
- 组件 CSS 中的硬编码十六进制值而非变量——搜索样式表中的原始十六进制代码并替换为变量
- SVG 图标中硬编码的
fill="#000000"——改为fill="currentColor" - 不响应
data-theme的第三方组件——用作用域 CSS 层封装它们 - 未设置
color-scheme属性——在:root上添加color-scheme: light dark,使浏览器原生 UI(滚动条、表单控件)也能适配 <head>中缺少<meta name="color-scheme">——添加它,使浏览器在 CSS 加载前能应用正确的背景色
<meta name="color-scheme" content="light dark">
:root {
color-scheme: light dark;
}
这一小小的添加,可使原生滚动条、日期选择器及其他操作系统渲染的表单控件自动切换到深色变体——很多实现都会忽略这个细节。
核心要点
- 在
:root上将所有颜色定义为 CSS 自定义属性,并使用[data-theme="dark"]为深色模式覆盖。组件样式只引用变量,一旦调色板建立,主题切换零成本。 - 使用
prefers-color-scheme: dark作为操作系统已设置深色外观的用户的自动默认值。在此基础上叠加带有localStorage持久化的 JavaScript 切换功能,供需要覆盖的用户使用。 - 在 CSS 加载前在
<head>中运行防闪烁脚本,避免首次绘制时出现错误主题的闪烁。 - 深色模式颜色不是反转的浅色模式颜色——降低极端对比度,用更浅的背景传达层级,并将品牌强调色适度降低饱和度以避免霓虹感。
- 使用对比度检查器验证每对文字与背景,使用色调生成器为两套主题的每种品牌色找到合适的色调。
- 添加
color-scheme: light dark及对应的<meta>标签,使浏览器原生 UI 元素(滚动条、输入框)也能自动切换。