暗色模式的正确打开方式：告别闪烁

大伙儿一般是怎么实现暗色模式的呢？多半是搞一个 ThemeProvider，用 React 状态来管理当前主题，再暴露一个 useTheme hook 供组件使用，主题偏好存到 localStorage 里持久化。这套方案逻辑上没毛病，但实际用起来会遇到一个经典问题——主题闪烁。闪烁从何而来？刷新页面时，你可能会看到页面先是默认主题（比如亮色），然后"闪"一下变成用户之前选的暗色。原因很简单：React 的加载时机太晚了。浏览器渲染 HTML 和 CSS 的速度远快于 JavaScript 的解析执行。等到 React 跑起来、从 localStorage 读到用户主题、再更新 DOM 的时候，页面早就渲染完第一帧了。这一帧用的是默认主题，下一帧才切到正确主题，于是就"闪"了。解法一：Inline Script 抢跑既然 React 太慢，那就让原生 JS 先跑。核心思路是：在 里注入一段同步执行的 inline script，它会在页面渲染之前读取 localStorage 并设置好主题 class。 const themeScript = ` (function() { try { const stored = localStorage.getItem("ui-theme") || "system"; const theme = ["light", "dark", "system"].includes(stored) ? stored : "system"; if (theme === "system") { const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; document.documentElement.classList.add(systemTheme, "system"); } else { document.documentElement.classList.add(theme); } } catch { // localStorage 不可用时，fallback 到系统主题 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; document.documentElement.classList.add(systemTheme, "system"); } })(); `; 在 TanStack Start 中，可以用 ScriptOnce 组件来注入： import { ScriptOnce } from "@tanstack/react-router"; export function ThemeProvider({ children }: { children: ReactNode }) { // ...状态管理逻辑 return ( {children} ); } 这样，页面首次渲染时 上就已经有了正确的 class（如 class="dark"），CSS 直接生效，不闪。解法二：CSS 读取主题状态 Inline script 解决了设置主题的时机问题，但还有个细节容易被忽略——主题切换按钮的图标。假设你的按钮要根据当前主题显示不同图标：亮色显示太阳、暗色显示月亮、跟随系统显示显示器。如果用 React 状态来控制图标渲染，同样会有闪烁问题，因为 React 还是那个"慢一拍"的 React。解决方法出奇地简单：让 CSS 来决定显示哪个图标。 CSS 的解析速度比 JS 快得多，而且我们的 inline script 已经保证了 上有正确的 class。只需要用 CSS 选择器匹配这些 class，就能在第一帧就显示正确的图标。在按钮组件里，用 Tailwind 的任意选择器语法让 CSS 控制图标显隐： import { Monitor, Moon, Sun } from "lucide-react"; export function ThemeToggle() { const { userTheme, setTheme } = useTheme(); // ...切换逻辑 return (
{/* 亮色模式（非跟随系统）：显示太阳 */} {/* 暗色模式（非跟随系统）：显示月亮 */} {/* 跟随系统：显示显示器 */}
); } 这里用到了 Tailwind 的任意值语法 [selector]，可以直接写原生 CSS 选择器。[.light:not(.system)_&]:block 的意思是：当祖先元素有 .light 类但没有 .system 类时，应用 display: block。 @custom-variant dark (&:is(.dark *)); @custom-variant light (&:is(.light *)); @custom-variant system (&:is(.system *)); 三个图标始终都在 DOM 里，但只有一个是 block，其余都是 hidden。CSS 根据 上的 class 瞬间决定谁显示，零闪烁。小结问题原因解法页面主题闪烁 React 加载晚于首次渲染 Inline script 提前设置主题 class 按钮图标闪烁 React 状态更新晚 CSS 选择器读取主题 class 核心思想就一句话：能用 CSS 解决的，就别等 JS。