用 CSS 变量 + prefers-color-scheme 实现暗色模式(含手动切换)

暗色模式现在是基础体验。正确做法是用 CSS 变量统一所有颜色,
然后 @media (prefers-color-scheme: dark) 覆盖变量。
手动切换则用 [data-theme="dark"] 选择器。

1. 颜色变量化

:root {
  --bg: #ffffff;
  --card: #f8f9fa;
  --text: #1a1a1a;
  --text-soft: #6b7280;
  --border: #e5e7eb;
  --primary: #2563eb;
  --primary-soft: #eff6ff;
}

body {
  background: var(--bg);
  color: var(--text);
}

.card {
  background: var(--card);
  border: 1px solid var(--border);
}

任何颜色都引用变量,绝不写死 color: #333

2. 自动跟随系统(最少代码)

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f172a;
    --card: #1e293b;
    --text: #f1f5f9;
    --text-soft: #94a3b8;
    --border: #334155;
    --primary: #60a5fa;
    --primary-soft: #1e3a8a;
  }
}

只这样:暗色模式自动跟系统设置变。

3. 手动切换(覆盖系统设置)

[data-theme] 优先级:

:root { --bg: #fff; /* ... */ }

@media (prefers-color-scheme: dark) {
  :root { --bg: #0f172a; /* ... */ }
}

[data-theme="dark"] {
  --bg: #0f172a; /* ... */
}

[data-theme="light"] {
  --bg: #ffffff; /* ... */
}

JS:

function setTheme(theme) {  // 'dark' | 'light' | 'system'
  if (theme === 'system') {
    document.documentElement.removeAttribute('data-theme')
    localStorage.removeItem('theme')
  } else {
    document.documentElement.setAttribute('data-theme', theme)
    localStorage.setItem('theme', theme)
  }
}

// 启动时还原
const saved = localStorage.getItem('theme')
if (saved) document.documentElement.setAttribute('data-theme', saved)

4. 避免 FOUC(First Of Unstyled Content)

如果上面的代码放在 <script> 模块末尾,会有一闪而过的错误主题。
解决:把"还原主题"提前到 <head> 顶部、inline 脚本:

<head>
  <script>
    (function() {
      var saved = localStorage.getItem('theme')
      if (saved) document.documentElement.setAttribute('data-theme', saved)
    })()
  </script>
  <!-- 后面才是 CSS -->
</head>

inline 脚本同步执行,CSS 应用前主题已就位。

5. 监听系统切换

用户用着用着把系统切到暗色——网页要立刻跟上:

const mq = window.matchMedia('(prefers-color-scheme: dark)')
mq.addEventListener('change', e => {
  // 只在没设手动主题时跟随
  if (!localStorage.getItem('theme')) {
    // CSS @media 会自动响应,这里不需要做什么,但如果你有 JS
    // 在用 mediaquery 判断当前主题,要刷新
    updateChartTheme(e.matches ? 'dark' : 'light')
  }
})

6. 图像 / iframe 内容怎么办

普通 <img> 没法跟随暗色。常用方案:

<picture>
  <source srcset="logo-dark.svg" media="(prefers-color-scheme: dark)">
  <img src="logo-light.svg" alt="Logo">
</picture>

SVG icon 是 CSS color 控制的(用 currentColor):

<svg class="icon" fill="currentColor">...</svg>
.icon { color: var(--text); }

iframe 内容(地图、第三方嵌入)就只能自己看是否支持 ?theme=dark
参数。

7. 配色技巧

不是简单地"颜色取反",几个要点:

  • 暗色背景不要纯黑(#000),用 #0d1117 / #0f172a 这种"接近黑"
    减少 contrast 疲劳
  • 卡片背景比 body 稍亮(如 #161b22)
  • 文字 #e6edf3 而不是 #fff,避免高对比刺眼
  • 主色 brand color 通常要在暗色下稍微 desaturate + lighten
  • shadow 在暗色下基本看不见,可以用 border 替代分隔

GitHub Dark / Tailwind slate 系列是优秀范本,直接借用色值。

8. 给暗色添加缓动

切换时颜色突变很扎眼,加 transition:

:root {
  /* ... */
}
body, .card, .button {
  transition: background-color .2s, color .2s, border-color .2s;
}

但别把 transition: all 加在所有元素上,会触发不必要的重排。

踩过的坑

  • 写死的 SVG 颜色 / inline style 是暗色模式的常见漏网之鱼。grep 整个
    代码库 #[0-9a-fA-F]{3,6} 看哪些没用变量。
  • 图表库 / 代码高亮通常自带主题,需要单独切。Pygments / Highlight.js
    都有 light + dark theme 包。
  • 系统暗色模式但用户手动选浅色——很多人不太会用,记得在 settings 里
    加 "跟随系统 / 强制亮色 / 强制暗色" 三选项。
  • 别忘了 favicon / meta[name=theme-color] 也支持 prefers-color-scheme:
    html <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)"> <meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。