CSS Custom Properties 在主题切换中的几个坑
暗色模式不只是换个颜色,CSS 变量切换时还要处理 transition、initial value、SSR 闪烁等问题。
1 分钟阅读
最近给博客加暗色模式,遇到几个不直观的坑,记下来。
1. 闪烁问题(FOUC)
最常见的坑:页面加载时,先用默认主题渲染了一帧,然后 JS 才切换到用户选择的主题,导致一闪而过。
解决方法:在 <head> 里加一段同步执行的 inline script,在第一次 paint 之前就把 dark 类名加上:
<script>
(function () {
const stored = localStorage.getItem('theme');
const prefersDark = matchMedia('(prefers-color-scheme: dark)').matches;
if (stored === 'dark' || (!stored && prefersDark)) {
document.documentElement.classList.add('dark');
}
})();
</script>
关键是 is:inline,否则 Astro 会把它当模块处理,等到 DOM 加载完才执行。
2. Transition 的副作用
我加了一个 transition: background-color 0.3s 在 body 上,切主题时整页有个柔和过渡。
但当用户滚动到一半切主题时,整页重绘过渡会卡顿。原因是 transition 触发了合成层重新分配。
解决:用 view-transition-api 替代 CSS transition,或者只在初始状态用 transition,切换瞬间临时禁用:
html.theme-changing * {
transition: none !important;
}
3. Tailwind v4 的 dark variant
Tailwind v4 默认 dark variant 是 prefers-color-scheme。要改成基于类名,得用 @custom-variant:
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
写完这个,dark: 修饰符才认 .dark 类。
4. 颜色 token 的层级
我习惯这样组织:
@theme {
--color-paper: #faf7f2;
--color-night: #0f0f0e;
}
:root {
--bg: var(--color-paper);
}
.dark {
--bg: var(--color-night);
}
Tailwind 的 @theme 是设计 token 层(不随主题变),:root / .dark 是语义层(随主题变)。组件只用语义变量,不直接消费设计 token。
5. 系统主题变化时跟随
如果用户没显式选过主题,应该跟随系统:
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
document.documentElement.classList.toggle('dark', e.matches);
}
});
加这一行,桌面切换深色模式时网站会自动跟着变,体验加分。
小结
暗色模式看着简单,做扎实要考虑:闪烁、过渡、SSR、Tailwind 适配、系统跟随。 不是难,是细节多。