CSS变量本身服务端不可见,但JS读取(如getComputedStyle)会因document不存在导致水合错误;必须用useEffect延迟到客户端执行,并提供降级值或默认状态。
CSS 变量(--my-color)本身不会触发水合错误,但当你用 JavaScript 读取或写入它们(比如 getComputedStyle(document.body).getPropertyValue('--my-color')),就进入了客户端专属领域。服务端渲染时,document 和 window 不存在,这段代码会报错或返回空值;而客户端执行时却能拿到真实值——两边输出不一致,React 检测到 DOM 文本或属性变化,立刻抛出 Text content does not match server-rendered HTML。
所有依赖 getComputedStyle、element.style 或 matchMedia 获取 CSS 变量的行为,必须包裹在 useEffect 或条件判断中,确保只在浏览器环境运行:
getComputedStyle
useEffect 内读取,并用 useState 初始化一个默认值用于服务端渲染useEffect 不会在服务端执行,所以它的逻辑天然“延迟”到水合之后,不会污染服务端输出示例:
'use client';import { useEffect, useState } from 'react';export default function ThemeAwareButton() {const [theme, setTheme] = useState('light'); // 服务端用默认值useEffect(() => {const root = document.documentElement;const value = getComputedStyle(root).getPropertyValue('--theme').trim();setTheme(value || 'light');}, []);return <button className={`btn-${theme}`}>{theme} mode</button>;}
如果你通过 JS 动态设置 CSS 变量(如 document.documentElement.style.setProperty('--color', '#fff')),这个操作只能发生在客户端。但服务端生成的 HTML 如果已包含依赖该变量的内联样式或 class 行为,就可能因变量未就位而渲染出不同结构。
style={{ color: `var(--text-color)` }} 同时又没提供 fallback)color: var(--text-color, #333),否则服务端渲染时该声明会被忽略,客户端才生效,造成文本颜色突变<html class="dark">),应使用 Next.js 的 dynamic + noSsr 或将该逻辑完全移入 Client Component像 dark-reader、font-awesome 或某些 UI 库的 theme provider,可能在客户端运行时动态向 :root 注入变量。这些变量服务端根本不可见,但你的组件如果直接依赖它们做条件渲染(比如 if (getVariable('--is-dark') === 'true') {...}),就会导致水合失败。
prefers-color-scheme 媒体查询 + useMediaQuery Hook'use client' 的自定义 hook)最易被忽略的一点:CSS 变量本身是纯样式的,但一旦你用 JS 去“观察”它、响应它、基于它分支渲染,它就不再是静态资源,而成了水合一致性链条上脆弱的一环——此时它和 window.innerWidth 没本质区别。