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 条评论

登录后可评论。

查看完整榜单
查看完整榜单
查看完整榜单