用 CSS 变量管理深浅色主题
给站点加深浅色切换,我前后改过三版。第一版写了两套完整的样式表,第二版用 prefers-color-scheme 媒体查询硬套,最后落到现在的方案:把颜色收进 CSS 变量,用 HTML 上的一个属性控制切换。
三种做法的问题
写两套样式表的问题是维护成本翻倍。改一个间距要改两个地方,时间一长必然漏掉一处,然后在某个深夜收到一条“深色模式下这里看不清”的留言。
只用媒体查询的问题是用户没法手动选。系统是深色,他就只能看深色,哪怕白天在阳光下看不清楚。
现在的做法
第一步,把所有颜色定义成变量,浅色放 :root,深色挂在 [data-theme='dark'] 上:
:root {
color-scheme: light;
--bg: #ffffff;
--text: #18181b;
--border: #e8e8ea;
}
[data-theme='dark'] {
color-scheme: dark;
--bg: #0f0f11;
--text: #f0f0f2;
--border: #26262b;
}
第二步,样式里只用变量,不写死颜色:
body {
background-color: var(--bg);
color: var(--text);
}
.card {
border: 1px solid var(--border);
}
这里有个容易被忽略的细节:color-scheme 不只是给浏览器看的提示。它决定滚动条、表单控件、输入框光标这些浏览器自己绘制的东西用浅色还是深色。漏掉这一行,深色页面里会出现一条刺眼的白色滚动条。
别让页面闪一下
切换逻辑本身很简单,真正麻烦的是首屏闪白:浏览器先按默认浅色渲染了一遍,脚本执行后才改成深色,用户会看到一次明显的白闪。
解决办法是把这段判断放在 <head> 的最前面,并且让它内联执行、不要被打包工具延迟:
<script is:inline>
(function () {
var theme = 'light';
try {
var saved = localStorage.getItem('theme');
if (saved === 'light' || saved === 'dark') {
theme = saved;
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
theme = 'dark';
}
} catch (error) {
/* 隐私模式下 localStorage 可能抛错,退回浅色 */
}
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
判断的优先级是:用户手动选过的 > 系统设置。前者存进 localStorage,后者读 matchMedia。
跟随系统变化
只在加载时读一次系统设置是不够的。macOS 和 Windows 都支持按时间自动切换深色,如果用户没手动选过,页面应该跟着变:
media.addEventListener('change', (event) => {
// 用户手动选过就不打断他
if (localStorage.getItem('theme')) return;
applyTheme(event.matches ? 'dark' : 'light');
});
几个小细节
- 切换按钮的图标用两个 SVG 叠加,靠 CSS 按当前主题显示其中一个,避免图标闪一下
- 主题色记得同步给
<meta name="theme-color">,手机浏览器地址栏颜色才会跟着变 - 过渡动画包在
prefers-reduced-motion: no-preference里,照顾对动效敏感的人
整套改下来,样式表里没有一个写死的颜色,加一套新主题只是再写一组变量的事。