css变量本身服务端不可见,但js读取(如getcomputedstyle)会因document不存在导致水合错误;必须用useeffect延迟到客户端执行,并提供降级值或默认状态。

Next.js 中 CSS 变量在服务端 vs 客户端的可见性差异
CSS 变量(--my-color)本身不会触发水合错误,但当你用 JavaScript 读取或写入它们(比如 getComputedStyle(document.body).getPropertyValue('--my-color')),就进入了客户端专属领域。服务端渲染时,document 和 window 不存在,这段代码会报错或返回空值;而客户端执行时却能拿到真实值——两边输出不一致,React 检测到 DOM 文本或属性变化,立刻抛出 Text content does not match server-rendered HTML。
避免在服务端尝试读取 CSS 变量值
所有依赖 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>;
}
CSS 变量定义本身也要区分 SSR/CSR 场景
如果你通过 JS 动态设置 CSS 变量(如 document.documentElement.style.setProperty('--color', '#fff')),这个操作只能发生在客户端。但服务端生成的 HTML 如果已包含依赖该变量的内联样式或 class 行为,就可能因变量未就位而渲染出不同结构。
- 不要在服务端组件里假设某个 CSS 变量“已经存在”并据此控制 DOM 结构(例如用
style={{ color: `var(--text-color)` }}同时又没提供 fallback) - 始终为 CSS 变量提供降级值:
color: var(--text-color, #333),否则服务端渲染时该声明会被忽略,客户端才生效,造成文本颜色突变 - 若需根据主题切换整页 class(如
),应使用 Next.js 的dynamic+noSsr或将该逻辑完全移入 Client Component
第三方库注入的 CSS 变量更危险
像 dark-reader、font-awesome 或某些 UI 库的 theme provider,可能在客户端运行时动态向 :root 注入变量。这些变量服务端根本不可见,但你的组件如果直接依赖它们做条件渲染(比如 if (getVariable('--is-dark') === 'true') {...}),就会导致水合失败。
- 这类场景下,不能靠 JS 读取变量来驱动逻辑,而应改用更稳定的信号:如
prefers-color-scheme媒体查询 +useMediaQueryHook - 或者把整个主题感知逻辑封装进一个 Client Component,彻底隔离服务端
- 检查构建产物中是否意外将客户端 CSS 注入逻辑跑到了服务端(常见于未加
'use client'的自定义 hook)
最易被忽略的一点:CSS 变量本身是纯样式的,但一旦你用 JS 去“观察”它、响应它、基于它分支渲染,它就不再是静态资源,而成了水合一致性链条上脆弱的一环——此时它和 window.innerWidth 没本质区别。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











