深浅色主题切换听起来像是要引个主题库的活,实际上它的最小实现只需要三样东西:CSS 变量、一个 data 属性、几行 JS。这个博客就是这么做的,下面是完整思路。

第一步:把颜色全部收进变量

主题切换的本质是换一组变量值,而不是换一套样式。所有组件只允许引用变量,不允许写死颜色:

css
:root {
  --bg: #faf9f5;
  --text: #2a2926;
  --accent: #8a6d3b;
}

[data-theme="dark"] {
  --bg: #191919;
  --text: #d6d3cb;
  --accent: #c9a96a;
}

关键点::root 放默认值(浅色),深色主题只写差异项。这样新增颜色时只需要想一次它属于哪组。

第二步:切换就是改一个属性

js
const root = document.documentElement
const toggle = () => {
  const next = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'
  root.setAttribute('data-theme', next)
  localStorage.setItem('theme', next)
}

CSS 的属性选择器 [data-theme="dark"] 完成剩下的所有工作。不需要类名拼接、不需要重渲染、不需要任何框架。

第三步:三个生产级细节

1. 防止暗色闪烁(FOUC)。 页面加载时如果默认浅色、用户偏好深色,会先闪一下白屏。解法是在 <head> 里放一段同步脚本,渲染前就定好主题:

html
<script>
  var t = localStorage.getItem('theme') ||
    (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
  document.documentElement.setAttribute('data-theme', t);
</script>

注意它必须不加 defer——同步执行发生在首次绘制之前。

2. 默认跟随系统。 没有用户选择记录时,用 prefers-color-scheme 媒体查询的判断结果兜底。用户手动切换后,选择存入 localStorage 优先级最高。

3. 尊重 CSS 层面同样有效的事实。 也可以完全不用 JS 切换、只靠媒体查询跟随系统,但那样用户就失去了手动选择权。JS + data 属性的方案两者兼得。

常见坑

  • 只换背景不换图:代码高亮、图片在深色背景下经常违和。代码块要单独准备一套深色 token 色
  • 半透明色没有跟着变:rgba(0,0,0,.05) 这类遮罩在两套主题下观感不同,阴影变量也要收进主题组
  • meta theme-color 不更新:移动端浏览器地址栏颜色跟随系统而不跟随站点主题,切换时顺手更新 <meta name="theme-color"> 体验更完整
  • 打印样式:@media print 里强制浅色,否则用户打印出来的是黑底

小结

CSS 变量 + data 属性是这个问题的正解,复杂度不会超过 30 行 JS。真正的工作量在第三步的细节里——防闪烁、跟系统、记忆选择,以及把整套视觉 token 认真拆成两组。