首帧闪屏源于css变量未在html解析早期就位,必须在内联js同步检测prefers-color-scheme并设置data-theme,同时双写[data-theme]和@media规则,并提供变量fallback。

只靠 @media (prefers-color-scheme: dark) 声明变量,必然闪——浏览器首次渲染时根本没读到系统偏好,:root 里的变量还是浅色值,等媒体查询生效再重绘,白→黑的跳变在 iOS 和 Android WebView 里尤其刺眼。
为什么初始渲染就闪,和 JS 执行时机无关
闪的本质是 CSS 变量未在 HTML 解析早期就位。哪怕你 JS 写得再快,document.documentElement 上没提前带 data-theme,浏览器就只能按默认(通常是浅色)走一遍样式计算和绘制。
-
@media规则本身不阻塞渲染,它只是“匹配后才应用”,而匹配发生在样式表解析后期 - 服务端渲染(SSR)页面更明显:HTML 已经吐出,JS 还没执行,
上没data-theme="dark",首屏必亮 - 不要把变量定义放在外链 CSS 里——网络延迟会让 fallback 颜色暴露更久
必须在 里内联轻量 JS 检测并设 data-theme
用 window.matchMedia 在 DOM 构建前读取当前偏好,并立刻写入 ,让 CSS 选择器能立即命中。
- 这段 JS 必须放在
最上方的<script></script>里,不能 defer,不能 module - 别只监听变更,先做一次同步检测:
const mql = window.matchMedia('(prefers-color-scheme: dark)'); document.documentElement.setAttribute('data-theme', mql.matches ? 'dark' : 'light'); - 后续再用
mql.addEventListener('change', ...)处理运行时切换,但首帧防闪只靠这次初始化
所有颜色规则必须同时写两套:[data-theme] + @media
单写 @media 是语义正确但体验崩坏;单靠 [data-theme] 是体验可控但失去系统级语义。二者缺一不可。
- 例如按钮背景:
[data-theme="dark"] .btn { background-color: #1a1a1a; }(防首帧闪) +@media (prefers-color-scheme: dark) .btn { background-color: #1a1a1a; }(保系统联动) - 变量也得双保险:
:root { --bg: #fff; }→[data-theme="dark"] { --bg: #1a1a1a; }+@media (prefers-color-scheme: dark) { :root { --bg: #1a1a1a; } } - 使用处必须加 fallback:
background: var(--bg, #ffffff);,否则变量未定义时会退成transparent或继承值,不是你想要的颜色
第三方组件主题失效?优先级和变量链不能断
Ant Design、Vant 这类库通常靠 JS 注入 .dark 类或动态改 :root 变量,和你的 @media 规则天然冲突。
- 覆盖它们的样式时,必须提高选择器优先级:
html[data-theme="dark"] .van-button而不是.van-button - 绝对不要对第三方组件加
!important——它会切断变量链,导致用户手动点击切换主题时完全无响应 - 检查库是否提供
theme-changed事件,监听后同步更新data-theme,避免 JS 主题状态和 CSS 状态错位
最易被忽略的是真机 WebView 兼容性:iOS WKWebView 在某些版本中首次加载时 matchMedia 返回 matches: false,即使系统已是深色,必须配合 localStorage 缓存上一次用户主动选择的主题作为兜底。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











