【笔记】web 黑夜模式通用适配方案
一、判断黑夜模式的方式
1.1 CSS 媒体查询判断黑夜模式的属性——prefers-color-scheme
prefers-color-scheme CSS 媒体特性用于检测用户是否有将系统的主题色设置为亮色或者暗色。 ——MDN
值:
no-preference:表示系统未得知用户在这方面的选项。在布尔值上下文中,其执行结果为 false。
light:表示用户已告知系统他们选择使用浅色主题的界面。
dark:表示用户已告知系统他们选择使用暗色主题的界面。
如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @media (prefers-color-scheme: light) { html, body { color: black; background-color: white; } }
@media (prefers-color-scheme: dark) { html, body { color: white; background-color: black; } }
|
1.1.1 兼容情况
除了 IE 外,其他主流浏览器基本都支持prefers-color-scheme。

Window 的 matchMedia() 方法返回一个新的 MediaQueryList 对象,表示指定的媒体查询字符串解析后的结果。返回的 MediaQueryList 可被用于判定 Document 是否匹配媒体查询,或者监控一个 document 来判定它匹配了或者停止匹配了此媒体查询。 ——MDN
window.matchMedia(xxx) 返回一个 listenable-like 对象 MediaQueryList, 它继承自 EventTarget, 这意味着可以通过直接它获得最新的 MediaQuery 检测情况:
1 2 3 4 5
| if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { } else { }
|
1.2.1 监听主题变化
与 CSS 媒体查询不同,matchMedia 我们需要监听主题的变化作出对应处理,如下所示:
1 2 3 4 5 6 7 8 9
| const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
darkModeQuery.addListener(e => { if (e.matches) { } else { } });
|
1.2.2 兼容情况
主流浏览器基本都支持matchMedia。

二、黑夜模式样式适配处理方案
2.1 CSS 纯媒体查询实现
适用场景:需要快速实现、无需用户自定义主题的项目。如
1 2 3 4 5 6 7
| @media (prefers-color-scheme: dark) { body { background-color: #1a1a1a; color: #e0e0e0; } }
|
优缺点分析
优点:
- 零 js 依赖,纯 css 实现
- 系统级实时响应(跟随系统设置即时切换)
- 维护成本最低
缺点:
2.2 CSS 变量 + 媒体查询
适用场景:中大型项目、需要多主题扩展、已使用 CSS 变量的代码库。
如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| :root { --bg-color: #ffffff; --text-color: #333333; }
@media (prefers-color-scheme: dark) { :root { --bg-color: #1a1a1a; --text-color: #e0e0e0; } }
body { background-color: var(--bg-color); color: var(--text-color); }
|
优缺点分析
优点:
缺点:
- 仍无法保存用户偏好
- 需要统一使用 CSS 变量规范
适用场景:需要用户自定义主题的 ToC 产品、重视用户体验的 Web 应用。
如:
1
| <button id="themeToggle">切换主题</button>
|
1 2 3 4 5 6 7 8 9
| body.light-mode { --bg-color: #ffffff; --text-color: #333333; }
body.dark-mode { --bg-color: #1a1a1a; --text-color: #e0e0e0; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const themeToggle = document.querySelector('#themeToggle'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
function initTheme() { const savedTheme = localStorage.getItem('theme') || (prefersDark.matches ? 'dark' : 'light'); document.body.classList.add(`${savedTheme}-mode`); }
themeToggle.addEventListener('click', () => { document.body.classList.toggle('dark-mode'); document.body.classList.toggle('light-mode'); const currentTheme = document.body.classList.contains('dark-mode') ? 'dark' : 'light'; localStorage.setItem('theme', currentTheme); });
prefersDark.addListener(e => { if (!localStorage.getItem('theme')) { document.body.classList.toggle('dark-mode', e.matches); } });
|
优缺点分析
优点:
- 支持用户偏好保存
- 同时响应系统和手动切换
- 最佳用户体验
缺点:
- 需要维护两套样式,依赖 js 处理。实现复杂度最高
适用场景:适合 React 等框架,已使用 CSS-in-JS 方案
如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import { createGlobalStyle, ThemeProvider } from 'styled-components';
const lightTheme = { bg: '#fff', text: '#333' }; const darkTheme = { bg: '#1a1a1a', text: '#e0e0e0' };
const GlobalStyle = createGlobalStyle` body { background: ${props => props.theme.bg}; color: ${props => props.theme.text}; } `;
function App() { const [isDark, setIsDark] = useState(window.matchMedia('(prefers-color-scheme: dark)').matches);
useEffect(() => { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const handler = e => setIsDark(e.matches); mediaQuery.addListener(handler); return () => mediaQuery.removeListener(handler); }, []);
return ( <ThemeProvider theme={isDark ? darkTheme : lightTheme}> <GlobalStyle /> {} </ThemeProvider> ); }
|
优缺点分析
优点:
- 完美配合组件化开发
- 主题状态可全局管理
- 支持动态主题切换
缺点:
- 强依赖特定框架
- 需要 CSS-in-JS 体系支持
2.5 *CSS 媒体查询 + filter 滤镜处理
适合场景:要求最快上线、或最小改动。本质其实也归属于方案 1(2.1)
如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @media (prefers-color-scheme: dark) { html { filter: invert(1) hue-rotate(180deg); }
img, video, .logo, .icon { filter: invert(1) hue-rotate(180deg); } }
|
滤镜设置解释
整体滤镜处理可以概括为:反色 + 调整色相
invert(1): invert() 函数用于反转输入图像中的颜色。参数定义了转换的程度。如果参数是 1(或者 100%),则会完全反转颜色,即每个颜色通道的值都会被替换为其补色。例如:黑色变成白色,白色变为黑色等。
- 当使用
invert(1) 时,则表示将图像的颜色彻底反转,即是:黑色变成白色,白色变为黑色。
hue-rotate(180deg): hue-rotate() 函数按照给定的角度旋转色彩轮上的颜色,其实就是冲淡颜色。这里的“角度”是指在标准色轮上转动多少度。色轮是一个圆形图表,显示了不同颜色如何根据它们的色调相互关联。
- 当使用
hue-rotate(180deg) 时,意味着所有颜色都会在其原始位置基础上沿着色轮顺时针方向移动 180 度。比如红色会变成青色、绿色变成洋红色、蓝色变成黄色等,因为这些是在色轮上相对的颜色。
更多信息可见MDN - invert、MDN - hue-rotate
兼容情况
除 IE 外,主流浏览器基本能支持。

优缺点分析
优点:
- 开发和维护成本都是最低
- 零 js 依赖,纯 css 实现
- 系统级实时响应(跟随系统设置即时切换)
缺点:
- 不能或很难自定义色值
- 不能保存用户偏好(比如用户想覆盖系统设置)
三、黑夜模式色值设计标准
黑夜模式的色值设计和黑夜模式的工程实现没啥关系,属于用户体验保障。
3.1 WCAG 标准
WCAG(Web Content Accessibility Guidelines) 是由 W3C 制定的国际无障碍标准。其中颜色设置也是一项重要标准。
法律意义:WCAG 在美国《ADA 法案》、欧盟《EN 301 549》等法规中被引用
一些具体要求
- 核心要求
| 元素类型 |
最小对比度 |
例外场景 |
| 普通文本(<18pt/24px) |
4.5:1 |
装饰性/禁用状态文本 |
| 大文本(≥18pt/24px) |
3:1 |
粗体大文本(≥14pt/18.5px 粗体) |
| 图形控件(图标/按钮) |
3:1 |
纯装饰性图形 |
- 计算公式
使用相对亮度(Luminance)公式:
1
| Contrast Ratio = (L1 + 0.05) / (L2 + 0.05)
|
其中:
- L1 = 较亮颜色的相对亮度(0~1)
- L2 = 较暗颜色的相对亮度(0~1)
实际开发中的应用
- 颜色选择示例
| 场景 |
通过示例 |
失败示例 |
| 白底黑字 |
#FFFFFF vs #000000 (21:1) |
#FFFFFF vs #666666 (5.74:1) |
| 深色模式 |
#1A1A1A vs #E0E0E0 (10.3:1) |
#333333 vs #999999 (3.31:1) |
- 调试工具
自动检测:
1 2 3 4 5
| # Chrome DevTools Styles面板 → 点击颜色选择器 → 显示对比度比率
# VS Code插件 "WCAG Color Contrast Checker"
|
在线工具:
- 设计技巧
1 2 3 4 5 6 7 8 9 10 11
| :root { --safe-dark-bg: #1a1a1a; --safe-light-text: #e0e0e0; --accent-color: #007bff; }
button:disabled { opacity: 0.6; }
|
深度注意事项
- 动态场景:
- 悬停/聚焦状态的对比度需保持合规
- 渐变/阴影覆盖区域需取最差值验证
- 字体特性:
- 300 以下字重需提高对比度要求
- 衬线字体可能需要额外对比度补偿
- 环境适配:
- 移动设备户外模式需考虑屏幕反光影响
- OLED 屏幕需验证 PWM 调光下的可读性
法律合规案例
- Target 诉讼案(2006):因对比度不足赔偿$6 百万
- Domino’s 披萨案(2019):网站对比度违规败诉
- 英国政府:强制要求所有公共网站通过 AA 认证
进阶建议
1.AAA 级目标:
- 无障碍测试:
1 2 3 4 5
| NVDA (Windows) / VoiceOver (Mac)
axe DevTools / Lighthouse
|
设计系统集成:
1 2 3 4 5 6 7 8 9 10 11
| addon-a11y: { config: { contrast: { thresholds: { AA: 4.5, AAA: 7 } } } }
|
通过严格遵循这些标准,可确保您的深色模式既美观又合规。建议将对比度检测纳入 CI/CD 流程,实现自动化保障。
相关链接