实现暗色模式时,通常会使用ThemeProvider和useTheme hook来管理主题,并将主题偏好存储在localStorage中。但是,这种方法可能会导致主题闪烁问题,即页面刷新时,会先显示默认主题,然后快速切换到用户之前选定的主题。解决这个问题的方法包括使用inline script在页面渲染之前设置主题class,以及使用CSS选择器读取主题class来决定显示哪个图标。通过这些方法,可以避免主题闪烁问题,实现更好的用户体验。
大伙儿一般是怎么实现暗色模式的呢?
多半是搞一个 ThemeProvider,用 React 状态来管理当前主题,再暴露一个 useTheme hook 供组件使用,主题偏好存到 localStorage 里持久化。
这套方案逻辑上没毛病,但实际用起来会遇到一个经典问题——主题闪烁。
闪烁从何而来?
刷新页面时,你可能会看到页面先是默认主题(比如亮色),然后"闪"一下变成用户之前选的暗色。
原因很简单:React 的加载时机太晚了。
浏览器渲染 HTML 和 CSS 的速度远快于 JavaScript 的解析执行。等到 React 跑起来、从 localStorage 读到用户主题、再更新 DOM 的时候,页面早就渲染完第一帧了。这一帧用的是默认主题,下一帧才切到正确主题,于是就"闪"了。
解法一:Inline Script 抢跑
既然 React 太慢,那就让原生 JS 先跑。
核心思路是:在 <head> 里注入一段同步执行的 inline script,它会在页面渲染之前读取 localStorage 并设置好主题 class。
在 TanStack Start 中,可以用 ScriptOnce 组件来注入:
这样,页面首次渲染时 <html> 上就已经有了正确的 class(如 class="dark"),CSS 直接生效,不闪。
解法二:CSS 读取主题状态
Inline script 解决了设置主题的时机问题,但还有个细节容易被忽略——主题切换按钮的图标。
假设你的按钮要根据当前主题显示不同图标:亮色显示太阳、暗色显示月亮、跟随系统显示显示器。如果用 React 状态来控制图标渲染,同样会有闪烁问题,因为 React 还是那个"慢一拍"的 React。
解决方法出奇地简单:让 CSS 来决定显示哪个图标。
CSS 的解析速度比 JS 快得多,而且我们的 inline script 已经保证了 <html> 上有正确的 class。只需要用 CSS 选择器匹配这些 class,就能在第一帧就显示正确的图标。
在按钮组件里,用 Tailwind 的任意选择器语法让 CSS 控制图标显隐:
这里用到了 Tailwind 的任意值语法 [selector],可以直接写原生 CSS 选择器。[.light:not(.system)_&]:block 的意思是:当祖先元素有 .light 类但没有 .system 类时,应用 display: block。
三个图标始终都在 DOM 里,但只有一个是 block,其余都是 hidden。CSS 根据 <html> 上的 class 瞬间决定谁显示,零闪烁。
小结
问题 | 原因 | 解法 |
|---|---|---|
页面主题闪烁 | React 加载晚于首次渲染 | Inline script 提前设置主题 class |
按钮图标闪烁 | React 状态更新晚 | CSS 选择器读取主题 class |
核心思想就一句话:能用 CSS 解决的,就别等 JS。