关键在于将主题逻辑提前至html解析阶段执行:通过head内联脚本读取localstorage或系统偏好,同步设置html元素的data-theme属性,确保css变量在首屏渲染前生效,避免闪屏。

关键在于把主题决定逻辑“抢”在 CSS 渲染之前执行,而不是等 React 或 DOM 加载完再操作。localStorage 本身只是存储介质,真正影响首屏是否闪烁的,是它被读取和应用的时机与位置。
主题类名必须挂载到 html 标签上
不要用 body 或某个 div 包裹主题样式。因为 HTML 解析是从上到下进行的, 是最早可操作的根节点。只有在这里设置 data-theme 或 class,CSS 规则才能在首个像素绘制前就生效。
- CSS 中定义:html[data-theme="dark"] { --bg: #121212; --text: #e0e0e0; }
- 切勿写成 body.theme-dark,否则浏览器先按默认样式渲染 body 内容,再重绘,必然闪屏
- 所有主题变量都通过 :root 或 html 作用域声明,确保全局可继承
内联脚本必须放在 head 最顶部
不能等 DOMContentLoaded、useEffect 或 useEffect + useState,那些都太晚了。必须用一段原生 JS,在
里立即执行:- 在 中插入 script 标签,内容为:
const saved = localStorage.getItem('theme');<br>if (saved) document.documentElement.setAttribute('data-theme', saved);<br>else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {<br> document.documentElement.setAttribute('data-theme', 'dark');<br>} - 这段代码会在 HTML 解析过程中同步运行,早于任何 CSS 文件加载和页面渲染
- 如果 localStorage 不可用(如 Safari 隐私模式),捕获 SecurityError 后直接 fallback 到系统偏好
切换时同步更新 DOM 和 localStorage
用户点击按钮后,要一次性完成两件事:改 html 属性 + 存值。顺序不能错,也不能异步延迟。
- 示例操作:
const html = document.documentElement;<br>const newTheme = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';<br>html.setAttribute('data-theme', newTheme);<br>localStorage.setItem('theme', newTheme); - 避免使用 classList.toggle,容易状态错乱;统一用 setAttribute 控制 data-theme
- 不推荐存布尔值或数字,只存语义明确的字符串,如 'light' / 'dark' / 'blue'
支持系统偏好自动回退,但不覆盖用户主动选择
首次访问无 localStorage 时,应尊重系统设置;但一旦用户手动点过切换按钮,就以 localStorage 为准,不再受系统变化干扰。
- 监听系统变化可选,仅用于“跟随系统”开关开启时:
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {<br> if (!localStorage.getItem('theme')) {<br> document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');<br> }<br>}); - “跟随系统”开关点击时,应清除 localStorage.setItem('theme', ''),让逻辑回归初始判断路径
- 这样既保底,又不越权——用户的选择永远优先级最高










