必须用css变量分层+媒体查询兜底+js初始化防闪屏三者协同;仅@media (prefers-color-scheme: dark)会导致ios/android webview首次渲染白屏闪动,因:root变量已按浅色求值完毕,且变量须全定义在:root、深色覆盖须在媒体查询内重设:root、漏fallback会退化为初始值。

直接用 color-contrast() 不行——它还在草案阶段,2026 年 10 月主流浏览器仍不支持。真正在移动端稳定可用的方案,是靠 CSS 变量分层 + 媒体查询兜底 + JS 初始化防闪屏,三者缺一不可。
为什么不能只写 @media (prefers-color-scheme: dark) 就完事
单纯媒体查询会触发“白屏→黑屏”闪动,尤其在 iOS Safari 和 Android WebView 中明显。原因不是 JS 慢,而是浏览器首次渲染时,:root 变量已按浅色值求值完毕,等媒体查询生效才重算——这中间没有回退机制。
- 变量必须全部定义在
:root,不能塞进.dark或body里,否则表单控件、伪元素、Shadow DOM 内部读不到 - 深色覆盖必须写在
@media (prefers-color-scheme: dark) { :root { ... } }内部,而不是套一层.dark类再设变量 - 漏掉任意一个变量 fallback(比如
--border在深色下没定义),该属性就会退化成初始值,导致样式断裂
如何让变量“动态计算”出合理对比度,又不依赖 JS
CSS 本身不支持运行时颜色运算,所谓“动态计算”其实是靠语义化分层 + 系统关键词兜底。关键不是算,而是选对基准色。
- 优先用系统语义色:
Canvas、CanvasText、ButtonFace,它们随系统主题自动切换,且天然满足 WCAG 对比度要求 - 自定义色必须带亮度层级:比如用
lch(30% 20 270)定义主色,比#1e1e1e更可控;深色模式下改亮度值(lch(20% 20 270)),而非硬换 HEX - 禁用
var(--bg, #fff)这类 fallback 到亮色值的写法——深色模式下 fallback 会失效,直接显白底
移动端手动切换时,怎样避免和系统偏好冲突
用户点“深色按钮”后,页面不能被系统下次切回浅色时覆盖。核心是用 data-theme 属性接管最高优先级,媒体查询仅作 fallback。
- CSS 规则顺序必须是:
[data-theme="dark"] .btn→@media (prefers-color-scheme: dark) .btn,靠层叠优先级保证手动开关赢 - JS 初始化必须在
内联执行:window.matchMedia('(prefers-color-scheme: dark)').matches立即设置document.documentElement.setAttribute('data-theme', 'dark') - 监听
localStorage而非class:存取theme键,避免和第三方组件库(如 NutUI 的.nut-theme-dark)打架
真正容易被忽略的是表单控件和 SVG 图标——它们默认不继承 CSS 变量,必须显式加 color-scheme: light dark 告诉浏览器:“我支持双模式”,否则 iOS 上输入框边框、滚动条永远卡在浅色。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











