用 CSS 变量做一套可切换的明暗主题
不依赖任何框架,几十行 CSS 变量就能实现优雅、不闪烁的深色模式。
深色模式不该需要一个庞大的主题库。CSS 自定义属性(变量)几乎能独立搞定,本文就是这套方案的拆解。
一切颜色都来自变量
第一步,把所有颜色都抽成变量,集中定义在 :root:
:root {
--bg: #ffffff;
--text: #0d0f14;
--accent: #2f6bff;
--border: #e9ebf0;
}
[data-theme='dark'] {
--bg: #0b0d12;
--text: #f2f4f8;
--accent: #5b86ff;
--border: #222632;
}
之后所有样式只引用变量,绝不写死颜色:
body {
background: var(--bg);
color: var(--text);
}
切换主题,就只是在 <html> 上改一个 data-theme 属性,整站颜色瞬间联动。
切换的那几行 JS
const next =
document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
document.documentElement.dataset.theme = next;
localStorage.setItem('theme', next);
localStorage 记住用户的选择,下次访问还在。
解决「闪一下」的问题
最容易被忽略的坑:页面加载时先显示浅色,再「啪」地跳到深色。原因是设置主题的脚本跑得太晚。
解决办法——把这段脚本内联在 <head> 最前面,让它在首次绘制前就执行:
<script is:inline>
const saved = localStorage.getItem('theme');
const prefersDark = matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.dataset.theme =
saved || (prefersDark ? 'dark' : 'light');
</script>
关键是「在绘制前」。哪怕只晚一帧,用户都会看到那一下刺眼的闪烁。
顺手做的两件小事
- 用
color-mix()从主色派生出半透明的背景色,省得再单独定义一堆变量。 - 给
prefers-reduced-motion的用户关掉过渡动画,照顾前庭敏感的人。
几十行 CSS,零依赖,换肤丝滑。有时候平台原生的能力,比我们以为的强得多。