用 CSS 自定义属性做深浅色主题切换,不用任何框架
深浅色主题切换听起来像是要引个主题库的活,实际上它的最小实现只需要三样东西:CSS 变量、一个 data 属性、几行 JS。这个博客就是这么做的,下面是完整思路。
第一步:把颜色全部收进变量
主题切换的本质是换一组变量值,而不是换一套样式。所有组件只允许引用变量,不允许写死颜色:
:root {
--bg: #faf9f5;
--text: #2a2926;
--accent: #8a6d3b;
}
[data-theme="dark"] {
--bg: #191919;
--text: #d6d3cb;
--accent: #c9a96a;
}
关键点::root 放默认值(浅色),深色主题只写差异项。这样新增颜色时只需要想一次它属于哪组。
第二步:切换就是改一个属性
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> 里放一段同步脚本,渲染前就定好主题:
<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 认真拆成两组。