Dark Mode with Web Standards:系统偏好 + 手动覆盖的完整方案
暗色模式别再各端各写了:用 Web 标准把系统偏好和手动覆盖统一掉
导语
你可能见过不少站点“亮色/暗色”两套 CSS,iOS 和 Android 还要各写一套,结果手动开关一加,经常和系统偏好打架。这篇文章把问题拆清楚:浏览器到底给了哪些原生能力、手动覆盖该怎么介入、一套代码怎么同时兼顾系统偏好、用户选择和可访问性。
正文
1. 问题:你以为的“暗色模式”,其实是三件事混在一起
前端常说的暗色模式,通常其实是三个职责:
- 系统偏好:用户操作系统已经设成深色;
- 站点默认:站点先按系统偏好自动适配;
- 手动覆盖:用户点了一个开关,要优先按用户选择。
最常见的坑是:只写 @media (prefers-color-scheme: dark),结果用户一旦手动切回亮色,页面还是跟着系统走。另一个坑是把颜色硬编码进组件,导致手动覆盖要到处 !important。
2. 方案:用 color-scheme、CSS 变量和本地存储三层对齐
先把能力分层,再拼起来:
第一层:让浏览器知道站点支持什么颜色方案
:root {
color-scheme: light dark;
}
color-scheme 告诉浏览器这个页面接受亮色和暗色,滚动条、表单控件、高亮色都会跟着变,不用你再一个个覆盖。
第二层:用 CSS 变量定义两套主题
:root {
--bg: #fff;
--text: #111;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #111;
--text: #eee;
}
}
[data-theme="dark"] {
--bg: #111;
--text: #eee;
}
[data-theme="light"] {
--bg: #fff;
--text: #111;
}
这里最关键的是优先级顺序:手动覆盖的 data-theme 属性放在后面,自然比媒体查询更高。
第三层:JS 只做持久化和同步
const STORAGE_KEY = 'theme';
const root = document.documentElement;
function applyTheme(theme) {
root.setAttribute('data-theme', theme);
}
function initTheme() {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
applyTheme(stored);
return;
}
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
applyTheme(prefersDark ? 'dark' : 'light');
}
initTheme();
document.querySelector('#theme-toggle')?.addEventListener('click', () => {
const current = root.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
applyTheme(next);
localStorage.setItem(STORAGE_KEY, next);
});
这段代码的意图很明确:本地存储优先,系统偏好作为默认值。
3. 坑和收尾
- 别在 `:root` 里同时写 `color-scheme: only light` 又写暗色变量,这会自相矛盾;
- 如果需要高对比度模式,可额外监听 `prefers-contrast`;
- 如果站点有打印样式,记得在 `print` 媒体查询里固定为亮色变量;
- 手动开关建议用 `data-theme` 而不是 class,语义更明确,调试也方便。
收尾
如果你现在还在用两套独立 CSS 文件做暗色模式,建议先按这套思路改成单文件变量方案。判断标准很简单:手动切换后,页面是否能立即生效,并且刷新后依然保留用户选择。
评论区
0 条评论
登录后可评论。











