直接调用window.matchMedia('(prefers-color-scheme: dark)').matches是目前最可靠的方案——它比依赖CSS类名或localStorage模拟主题要轻量得多。但这里有个常见误区:很多开发者只在页面加载时读取一次,一旦系统主题动态切换,UI便无法同步更新。因此必须持续监听,并且监听方式需兼容不同浏览器版本,例如Safari 14以下只能使用addListener作为降级方案。
关于DOM调整的位置,千万别只挂在上就完事了。这样容易与第三方组件的样式产生冲突,而且伪元素、SVG图标、甚至原生表单控件(如)都不会随主题变化。
推荐的正确做法是:将dark类添加到根节点上,同时声明。这样浏览器就能识别你支持双模式,原生滚动条、表单控件会自动适配。
还有一个关键细节:许多图标字体(比如Font Awesome)依赖:root下的CSS变量,因此:root.dark中的CSS变量重定义必须完整,仅仅靠class覆盖是不够的——CSS变量不会随class自动切换。
问题是:CSS变量切换颜色虽然高效,但对图片、SVG内联fill、canvas绘图完全没有作用。它们不会自动换色。
什么场景下必须操作DOM?
- SVG中使用
时,需要根据当前模式将href替换为#icon-moon。 - 图片资源也要切换——使用
元素配合实现自动替换。 - 第三方图表库(例如Chart.js)渲染后无法直接重绘主题,只能销毁再重新初始化,所以外层容器需要添加
data-theme属性供JS识别。
接下来要处理用户手动切换主题的情况。用户主动切换后存入localStorage,当系统从暗色切回浅色时,页面仍保持暗色——这并非bug,而是预期行为。但要妥善处理同步逻辑:
- 初始化时优先读取
localStorage.theme,若不存在则回退到matchMedia。 - 监听系统变化时,只有当localStorage中没有存储值时,才自动更新页面主题。
- 提供“跟随系统”开关,点击后清空localStorage,并手动触发一次
matchMedia同步。 - 不要存储布尔值,应存储字符串'light'/'dark'/'auto',便于后续扩展。
最后也是最容易被忽略的:服务端渲染(SSR)首次加载时,JavaScript尚未运行,matchMedia根本无法使用。此时需要借助,并结合服务端UA和IP地理位置做粗略判断,否则首屏会出现闪烁(白屏或黑屏)——这确实会严重影响用户体验。
