JS主导主题切换需用matchMedia+data-theme,避免内联样式;SSR需保护window访问,组件卸载时清除监听器,CSS用[data-theme]选择器响应。

不能完全不依赖 CSS,但可以做到 JS 主导、CSS 仅作变量容器——关键在于把样式计算和 DOM 状态更新全收在 JS 里,不让媒体查询直接控制渲染。
为什么「不依赖 CSS」本身是个伪命题
prefers-color-scheme 的匹配逻辑由浏览器原生实现,window.matchMedia 只是暴露这个能力的 JS 接口;它本身不改样式,也不触发重绘。所谓“不依赖 CSS”,实际是指:不靠 @media (prefers-color-scheme: dark) 自动应用样式规则,而是用 JS 读取 matches 后,主动写入 data-theme、切换 class 或注入内联 style。
这么做有明确代价:@media 是零 JS、首屏直出、无闪动的;而纯 JS 分发必须等脚本执行,SSR 场景下必然出现主题错位或闪动。
用 matchMedia + data-theme 实现运行时分发
这是最可控、兼容性最好、且与现有 CSS 架构解耦的方式。JS 只负责状态同步,CSS 用属性选择器响应:
立即学习“前端免费学习笔记(深入)”;
- 初始化时立即读取
window.matchMedia('(prefers-color-scheme: dark)').matches,设置document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light') - 绑定
change事件监听器,每次触发都重新设置data-theme属性(不要只切 class,避免和第三方库冲突) - CSS 中统一用
[data-theme="dark"]和[data-theme="light"]作用域写样式,不依赖@media规则 - 若需支持手动覆盖,优先读取
localStorage.getItem('color_mode'),仅当为空时 fallback 到matchMedia结果
绕过 CSS 变量、直接写内联 style 的风险点
有人试图用 element.style.setProperty('--bg-color', '#121212') 或 element.style.backgroundColor = '#121212' 实现“纯 JS 控制”,但这会快速失控:
- 内联
style优先级高于所有外部 CSS,一旦写入就极难被后续主题切换覆盖 - SVG 图标若已带
fill属性,var(--icon-color)就完全失效,必须提前清洗 DOM - 无法批量控制,每个元素都要单独遍历,性能差且易漏
-
matchMedia监听的是系统级变化,不是页面级交互,频繁操作内联样式可能触发强制同步布局
SSR 和内存泄漏是最容易被忽略的硬伤
服务端渲染时 typeof window === 'undefined',直接调用 matchMedia 会报错;客户端挂载后若没清理监听器,React/Vue 组件反复卸载再挂载会导致多个重复监听器堆积。
正确做法是:
- 加运行时保护:
if (typeof window !== 'undefined' && 'matchMedia' in window) - 保存 handler 引用,组件销毁时显式调用
mql.removeEventListener('change', handler) - 旧版 Safari(≤13.1)需回退到
mql.addListener(handler),但现代项目可放弃支持
真正难的不是监听,而是让 JS 分发逻辑和 CSS 渲染节奏对齐——尤其是用户刚打开页面那几百毫秒,别让它白忙一场。


















