首屏必闪是因为Sass编译出的CSS是静态的,主题变量在构建时已固化,而HTML初始渲染时缺少data-theme属性,导致浏览器先按默认主题渲染,JS后续设置才触发重绘。必须在最顶部内联JS同步读取localStorage和prefers-color-scheme,立即设置document.documentElement.setAttribute('data-theme', theme),并配合[data-theme="dark"]与@media双写规则及变量fallback。

为什么Sass编译出的多主题CSS首屏必闪
Sass本身不参与运行时,所有主题变量在构建时就被编译成静态CSS。如果你用@if $theme == 'dark'生成两套独立样式表,或靠.dark类控制不同规则,那首屏闪的本质是:HTML吐出时,上还没带dark类或data-theme属性,浏览器只能按默认(通常是浅色)渲染一次,等JS执行完才补上类——这中间就是白→黑/黑→白的跳变。
常见错误现象:Failed to load resource: net::ERR_ABORTED(切换link.href)、文字先亮后暗、按钮边框颜色突变、body背景闪一下再变深。
- 别把主题判断逻辑全压给Sass——它无法响应用户实时偏好或localStorage值
- 别用
@import 'themes/dark.scss'配合JS动态加载CSS文件,网络延迟必然暴露fallback色 - 别在
:root里只写@media (prefers-color-scheme: dark),媒体查询不阻塞首帧渲染
Sass项目必须内联的三行JS检测逻辑
这段代码必须放在最顶部的<script></script>里,不能defer、不能type="module",否则赶不上首帧样式计算。
const mql = window.matchMedia('(prefers-color-scheme: dark)');
const saved = localStorage.getItem('theme');
const theme = saved || (mql.matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
注意:这里用data-theme而非class,是为了和Sass生成的[data-theme="dark"]选择器对齐;saved优先级高于系统偏好,符合“用户选择 > 系统设置 > 默认”的合理链路。
- 不要只监听
change事件,首帧防闪只靠这次同步设置 - 不要用
document.body.classList.add(),层级更高,能覆盖伪元素和::backdrop - 如果用了Next.js,直接换
next-themes,它已封装好这套逻辑并处理SSR hydration不一致问题
Sass中主题变量必须双写[data-theme]和@media
只靠@media (prefers-color-scheme: dark)声明变量,等于把首帧命运交给浏览器解析时机——它不阻塞渲染,只是“匹配后才应用”。而[data-theme="dark"]选择器能在写入后立刻生效。
正确写法(Sass中):
:root {
--bg: #fff;
--text: #333;
}
[data-theme="dark"] {
--bg: #1a1a1a;
--text: #f8f9fa;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
--text: #f8f9fa;
}
}
- 所有用到
var(--bg)的地方,必须加fallback:background-color: var(--bg, #fff),否则变量未定义时会退成transparent - 不要在Sass里用
!default做主题回退——它只在编译期起作用,运行时不生效 - 第三方组件(如Ant Design)的样式需用
html[data-theme="dark"] .ant-btn覆盖,避免被自己的@media规则忽略
transition必须写在基础选择器里,不是.dark类里
很多人在Sass中这样写:
.dark {
--bg: #1a1a1a;
transition: background-color 0.3s;
}
这是错的。transition只在类存在时才生效,初始状态没这个类,切换瞬间就没有过渡,颜色“啪”一下就变了。
正确做法是把transition写在通用选择器中:
body {
background-color: var(--bg, #fff);
color: var(--text, #333);
transition: background-color 0.3s ease, color 0.3s ease;
}
- transition作用对象是
background-color这类原生属性,不是--bg变量名本身 - 伪元素
::before/::after若用变量+transition,部分安卓WebView会延迟一帧,建议改用transform或opacity - 字体颜色切换时,
color必须显式声明transition,不能指望父级继承
关键点不在Sass怎么写,而在HTML解析早期是否已有确定的主题标识。变量可以晚一点更新,但data-theme属性必须在第一行CSS开始计算前就位——否则浏览器没得选,只能闪。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











