MUI palette 传 var() 直接炸:augmentColor 无法解析 CSS 变量的根因与正解
项目里已经有一套 Tailwind v4 + [data-theme] 属性驱动的 CSS 变量主题,深浅模式靠 <html data-theme="dark"> 切换,所有 token 都是 var(--color-*) 形式。这时需要引入两个 MUI 组件(Rating 星级 + Stepper 步骤条),直觉上只需要把调色板指向 CSS 变量就好了——于是写出了下面这段代码。
结果 createTheme 调用一跑,Rating 的填充星变成纯黑,Stepper 激活步骤颜色完全错误。切到暗色模式时,MUI 和自定义主题各行其是,相互打架。
现象
// 看起来无害的写法
const theme = createTheme({
palette: {
primary: { main: 'var(--color-primary)' },
},
});结果:
- 组件渲染颜色变为黑色或不可见
- hover / selected 状态叠加层颜色异常(变成实心色块而非半透明)
- 深浅模式切换后 MUI 颜色不跟随,和页面其他元素脱节
- Next.js App Router 下有 FOUC(样式闪烁)和 hydration 不匹配警告
根因
MUI 的 createPalette 在初始化时会对每个颜色 slot 调用 augmentColor。这个函数检查 light、dark、contrastText 是否存在——若缺失,它会调用 lighten()、darken()、getContrastText() 来推导出这三个值。
问题就出在这里:这些颜色数学函数接收的是真实的 hex/rgb/hsl 字符串,无法解析 var(--xxx) 这种字符串,只会得到 NaN 或抛异常,最终 fallback 成黑色。
同样的问题还发生在运行时:MUI 在渲染 hover overlay 时会调用 alpha(palette.primary.main, 0.08),alpha() 同样无法处理 var() 字符串,导致 hover 效果失效。
createTheme({ palette: { primary: { main: 'var(--x)' } } }) 是不够的。MUI 会用 lighten()/darken()/getContrastText() 推导出 light/dark/contrastText,这三个函数无法解析 CSS 变量字符串,结果是 NaN 或黑色。
同理,凡是 MUI 运行时调用 alpha(palette.x.main, n) 的地方(hover、selected 等叠加层),也都因为 var() 不可解析而炸掉。
正解
关键一:把 palette slot 的四个子值全部提供,augmentColor 发现全部存在就会跳过推导,直接使用你给的值:
import { createTheme } from "@mui/material/styles";
export const bridgeTheme = createTheme({
palette: {
primary: {
main: "var(--color-primary)",
light: "var(--color-primary-hover)", // 必须提供,否则触发 lighten(var())
dark: "var(--color-primary-hover)", // 必须提供,否则触发 darken(var())
contrastText: "var(--color-primary-foreground)", // 必须提供,否则触发 getContrastText(var())
},
background: { default: "var(--color-bg)", paper: "var(--color-surface)" },
text: {
primary: "var(--color-foreground)",
secondary: "var(--color-muted)",
disabled: "var(--color-muted)",
},
divider: "var(--color-border)",
action: {
// alpha() 同样不能解析 var(),改用 CSS color-mix 预先算好透明度
hover: "color-mix(in srgb, var(--color-foreground) 6%, transparent)",
selected: "color-mix(in srgb, var(--color-primary) 12%, transparent)",
active: "var(--color-foreground)",
disabled: "var(--color-muted)",
disabledBackground: "var(--color-surface-hover)",
},
},
shape: { borderRadius: 10 }, // ⚠️ 不能用 var(),MUI 拿它做数字计算
typography: { fontFamily: "inherit" },
});关键二:不设 palette.mode,不用 cssVariables: true。你的 [data-theme] 属性切换 CSS 变量,MUI 直接读取它们的实时值,自动跟随深浅模式,无需引入第二套黑暗模式机制。
关键三:Next.js App Router 需要 emotion 缓存提供器,防止 SSR/CSR 样式不一致:
// layout.tsx(Server Component)
import { AppRouterCacheProvider } from "@mui/material-nextjs/v16-appRouter"; // 版本号对应 Next.js major
export default function RootLayout({ children }) {
return (
<html>
<body>
<AppRouterCacheProvider options={{ key: "mui" }}>
<MuiBridgeProvider>{children}</MuiBridgeProvider>
</AppRouterCacheProvider>
</body>
</html>
);
}
// MuiBridgeProvider.tsx("use client")
import { ThemeProvider } from "@mui/material/styles";
export function MuiBridgeProvider({ children }) {
return <ThemeProvider theme={bridgeTheme}>{children}</ThemeProvider>;
}注意 @mui/material-nextjs 的子路径按 Next.js 大版本区分:v13-appRouter / v14-appRouter / v15-appRouter / v16-appRouter,版本错误会静默导入但行为不对。
- 每个 palette slot 的
main、light、dark、contrastText四个值全部提供,让augmentColor无需推导直接跳过 action.*的 alpha 叠加层改用color-mix(in srgb, var(--x) N%, transparent)在 CSS 层面预先完成透明度计算- 不设
palette.mode——用[data-theme]属性翻转 CSS 变量即可,MUI 天然跟随
一句话外卖
把 CSS 变量传给 MUI palette 时,任何缺失的子值都会触发颜色数学推导——必须把四个子值全部喂给它,或者用
color-mix把透明度计算前移到 CSS 层面,彻底绕开 MUI 的alpha()/lighten()/darken()。