返回博客

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。这个函数检查 lightdarkcontrastText 是否存在——若缺失,它会调用 lighten()darken()getContrastText()推导出这三个值。

问题就出在这里:这些颜色数学函数接收的是真实的 hex/rgb/hsl 字符串,无法解析 var(--xxx) 这种字符串,只会得到 NaN 或抛异常,最终 fallback 成黑色。

同样的问题还发生在运行时:MUI 在渲染 hover overlay 时会调用 alpha(palette.primary.main, 0.08)alpha() 同样无法处理 var() 字符串,导致 hover 效果失效。

坑:只传 main 会触发 augmentColor 颜色推导,而 var() 不可被推导

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,版本错误会静默导入但行为不对。

正解:四值全给 + action 用 color-mix + 不设 palette.mode
  • 每个 palette slot 的 mainlightdarkcontrastText 四个值全部提供,让 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()