暗色模式早就不是可选项了。一套完整的方案要满足四件事:跟随系统、可手动切换、记住用户选择、刷新时不闪白。全用 CSS 变量实现,成本比想象中低得多。

第一步:颜色全部变量化

:root {
  --bg: #faf7f2;
  --surface: #ffffff;
  --text: #2b2622;
  --accent: #d9480f;
}

[data-theme="dark"] {
  --bg: #171412;
  --surface: #201c19;
  --text: #ece7e1;
  --accent: #ff8a4d;
}

暗色不是把颜色反转过来看看就行——纯黑背景 + 纯白文字的对比度会刺眼,深灰底(如 #171412)配米白字(#ece7e1)更接近“夜间看书”的观感,主色也要适当调亮提饱和。

第二步:让浏览器知道当前主题

:root { color-scheme: light; }
[data-theme="dark"] { color-scheme: dark; }

这一行管的是滚动条、表单控件这类浏览器原生 UI 的配色,不设的话暗色页面会顶着一根亮色滚动条,出戏。

第三步:默认跟随系统,切换后记忆

// 页面头部内联,防首屏闪白(FOUC)
(function () {
  var saved = localStorage.getItem('theme');
  var sys = matchMedia('(prefers-color-scheme: dark)').matches;
  document.documentElement.dataset.theme =
    saved || (sys ? 'dark' : 'light');
})();

// 切换按钮
function toggleTheme() {
  var cur = document.documentElement.dataset.theme;
  var next = cur === 'dark' ? 'light' : 'dark';
  document.documentElement.dataset.theme = next;
  localStorage.setItem('theme', next);
}

关键在内联在 head 里、阻塞渲染——如果这段代码放在外部 JS 里,浏览器会先按亮色画出第一帧再变暗,用户每次刷新都闪一下,体验非常廉价。

两个细节

  • 图片:亮白的截图在暗色下非常扎眼,给它们加一层柔光:[data-theme="dark"] img { filter: brightness(.85); }
  • 系统主题变了要不要跟着变:监听 change 事件时,建议只在用户没有手动选择过(localStorage 为空)时跟随,尊重用户最后一次的明确意志。

本站的右上角那个小太阳/小月亮就是这么实现的,可以点着玩。