[material-ui] Composing a theme with multiple `experimental_extendTheme` calls throws error
Steps to reproduce
Link to live example: https://codesandbox.io/p/sandbox/mui-extendtheme-chaining-bug-6j9mps
Steps:
- create any new react project & install MUI
- call
experimental_extendThememore than once, and passing in aThemeobject for any of the calls
Current behavior
Chaining calls to experimental_extendTheme results in the following error:
MUI: `vars` is a private field used for CSS variables support. Please use another name.
Expected behavior
Should work without throwing an error. Matching createTheme functionality.
Context
Chaining calls to "compose" a theme object as described in the 2nd example here (but with the new extendTheme experimental feature instead)
Your environment
I just copied the codesandbox files to my machine to get these specs, not sure if that matters. Error still shows up when running it on my machine instead of codesandbox.
System: OS: macOS 14.5 Binaries: Node: 20.9.0 - ~/.nvm/versions/node/v20.9.0/bin/node npm: 10.1.0 - ~/.nvm/versions/node/v20.9.0/bin/npm pnpm: 8.5.0 - /usr/local/bin/pnpm Browsers: Chrome: 125.0.6422.113 Edge: Not Found Safari: 17.5 npmPackages: @emotion/react: 11.11.4 => 11.11.4 @emotion/styled: 11.11.5 => 11.11.5 @mui/base: 5.0.0-beta.40 @mui/core-downloads-tracker: 5.15.19 @mui/material: 5.15.19 => 5.15.19 @mui/private-theming: 5.15.14 @mui/styled-engine: 5.15.14 @mui/system: 5.15.15 @mui/types: 7.2.14 @mui/utils: 5.15.14 => 5.15.14 @types/react: 18.2.38 => 18.2.38 react: 18.2.0 => 18.2.0 react-dom: 18.2.0 => 18.2.0 typescript: 4.4.4 => 4.4.4
Search keywords: cssvars experimental_extendTheme extendtheme css variables experimental
Why do you need to call extendTheme based on the existing theme? It's not designed to be used like this. If you need multiple themes, they should be independent or you should extract shared tokens.
const tokens = { … };
cons theme1 = extendTheme(tokens);
const theme2 = extendTheme(tokens);
It's not designed to be used like this
Is it not supposed to mimic createTheme? According to the docs here:
When the value for a theme option is dependent on another theme option, you should compose the theme in steps.
let theme = createTheme({ palette: { primary: { main: '#0052cc', }, secondary: { main: '#edf2ff', }, }, }); theme = createTheme(theme, { palette: { info: { main: theme.palette.secondary.main, }, }, });
The app I'm trying to migrate from createTheme to extendTheme uses this idea of constructing the theme in steps. I know it's still experimental but the docs kind of imply that extendTheme and createTheme can be 1:1 swapped for each other
Having the same issue over here. My use case is that I have a function that is used to automatically configure locales for various themes. So it takes a language and an existing theme object and adds the locales to it. Works with createTheme, but getting the error above when trying to switch to the new extendTheme.
export const useThemeWithLocale = (
language: LanguageType,
theme: CssVarsTheme,
...args: object[]
): CssVarsTheme => {
const muiLanguage = language.replace(/-/g, '');
return useMemo(
() =>
extendTheme(
// current theme
theme,
// core translations
muiBaseLocales[muiLanguage],
// current language
{ language },
...args,
),
[args, language, muiLanguage, theme],
);
};
My current workaround is to take a CssVarsThemeOptions argument instead, but this is less than ideal since not all of the themes that we are dealing with have a separate options object created for them (we don't manage the code for all of the themes that we accept into this function).
@jcohen14 @joebochill Before I propose any solution, can you provide more details on the context of your apps? Ideally (with Material UI v6, you don't need to create multiple themes. It produces unnecessary complexity to your apps.
I can speak to my use case:
Our company has a design system, built around MUI. Our team provides standard themes for multiple other product-development teams to use for their applications. But not all products use the exact same theme. For example. some products are sold under a different brand name (white-labeled) — in this case, they made need to change things like the color palette, but they want to preserve all of the other aspects of our theme (component style overrides, etc.).
The localization case I mentioned in my other comment is related to a utility we provide to add localization to an existing theme. So if one of the teams we support built their own theme, but wanted to use the same localization behavior, we wouldn't be able to extend their theme if they had already called extendTheme to create it.
I think both of these cases can be covered following your recommended approach of splitting out shared tokens/config and then only calling extendTheme once. It just shifts some of the burden of knowing this nuance/behavior to the 'application' teams rather than our core team.
It seems like a bit of a misnomer to call it extendTheme when you can't extend an existing theme...you can only extend a set of ThemeOptions/config.
My company has set up our app to split the config object that gets passed to extendTheme (currently we're still using createTheme) into separate files for improved maintainability. So we rely heavily on the idea of building one singular theme in sequence. We first build a theme that contains all our tokens (palette, typography, etc) then use that theme to build the rest of our theme with access to utils like theme.spacing.
i also agree with @joebochill that if it cannot extend an existing theme, extendTheme is a bit of a misnomer.
We have a similar issue -- prior to v6, we were also using theme composition to access custom palette colors in component overrides.
For us, the solution was refactoring our component override files -- but I agree that extendTheme is a misnomer, and the docs on theme composition should be updated.
A component file:
const MuiCard: Components<Omit<Theme, "components" | "palette"> & CssVarsTheme> = {
MuiCard: {
styleOverrides: {
root: ({ theme }) => ({
borderRadius: 8,
border: `1px solid ${theme.palette.divider}`,
"& .card-more-options": {
marginTop: theme.spacing(-1),
marginRight: theme.spacing(-3),
},
"& .MuiTableContainer-root, & .MuiDataGrid-root, & .MuiDataGrid-columnHeaders":
{
borderRadius: 0,
},
}),
},
defaultProps: {
elevation: 0,
},
}
}
Theme creation:
let theme = extendTheme(
{
colorSchemeSelector: "class",
colorSchemes: {
dark: {
palette: darkPalette,
},
light: {
palette: lightPalette,
},
},
direction,
typography,
breakpoints,
shape: {
borderRadius: 3,
},
components: {
...MuiCard,
...MuiDataGrid,
[ETC]
},
},
);
After the release of v6 and trying to implement variable css it also throws the same error Error: MUI: vars is a private field used for CSS variables support.
Full error here
⨯ Error: MUI: `vars` is a private field used for CSS variables support.
Please use another name.
at eval (./src/providers/theme/navTheme.tsx:16:118)
at (ssr)/./src/providers/theme/navTheme.tsx (/Users/jairalexandretorrescaviedes/Desktop/BODYTECH_PROJECT/FRONT/Web-Corporate-Portal/.next/server/app/page.js:1336:1)
at __webpack_require__ (/Users/jairalexandretorrescaviedes/Desktop/BODYTECH_PROJECT/FRONT/Web-Corporate-Portal/.next/server/webpack-runtime.js:33:43)
at eval (./src/components/Header/index.tsx:16:83)
at (ssr)/./src/components/Header/index.tsx (/Users/jairalexandretorrescaviedes/Desktop/BODYTECH_PROJECT/FRONT/Web-Corporate-Portal/.next/server/app/page.js:896:1)
at Object.__webpack_require__ [as require] (/Users/jairalexandretorrescaviedes/Desktop/BODYTECH_PROJECT/FRONT/Web-Corporate-Portal/.next/server/webpack-runtime.js:33:43)
digest: "2385216222"
When you create a normal theme and create a new one using the first one as a base, it works fine but once variable css is enabled it stops working
This is how it is implemented:
// src/providers/theme/theme.tsx
'use client'
import { Chip, IconButton, createTheme } from '@mui/material'
import { esES } from '@mui/material/locale'
import { esES as esESX } from '@mui/x-date-pickers/locales'
import type {} from '@mui/x-date-pickers/themeAugmentation'
import * as assets from '@/assets/icons'
import { CALENDAR_MAX_DATE, CALENDAR_MIN_DATE } from '@/constants/misc'
import { Gotham } from '../../app/ui/fonts'
/**
* Represents the theme configuration for the application.
*/
export const theme = createTheme(
{
cssVariables: { cssVarPrefix: 'bt' },
components: {...},
palette: {
mode: 'light',
action: {
active: '#E7662B',
hover: '#ED8C60',
selected: '#E7662B',
disabled: '#EAEAEA',
disabledBackground: '#CBCACA',
focus: '#FDF0EA',
},
divider: '#CBCACA',
primary: { main: '#E7662B', dark: '#E7662B', light: '#ED8C60', contrastText: '#fff' },
secondary: { main: '#2B2D2E', dark: '#2B2D2E', light: '#626160', contrastText: '#fff' },
text: { primary: '#2B2D2E', secondary: '#626160', disabled: '#CBCACA' },
success: { main: '#B1CDBC', dark: '#B1CDBC', light: '#94c97a', contrastText: '#2B2D2E' },
error: { main: '#EC6969', dark: '#EC6969', light: '#F7C3C3', contrastText: '#fff' },
info: { main: '#a4e0ff', dark: '#C2F0F0', light: '#C2F0F0', contrastText: '#2B2D2E' },
warning: { main: '#f4e59f', dark: '#FFE4B2', light: '#FFE4B2', contrastText: '#2B2D2E' },
common: { black: '#2B2D2E', white: '#fff' },
gray: { main: '#eaeaea', light: '#f5f4f4', dark: '#cbcaca', contrastText: '#2b2d2e' },
},
shape: { borderRadius: 10 },
typography: {...},
},
esES,
esESX
)
// src/providers/theme/navTheme.tsx
'use client'
import { type Theme, createTheme } from '@mui/material'
import { useMemo } from 'react'
import { theme } from './theme'
/**
* Represents the theme for the nav bar when it is light
* @extends theme - The base theme
* @see [theme](../theme/theme.tsx)
*/
const navThemeLight = createTheme(theme, {
cssVariables: {cssVarPrefix: 'bt-nav-light',},
components: {...},
palette: {
secondary: { main: '#fff', dark: '#fff', light: '#f5f4f4', contrastText: '#2B2D2E' },
background: { default: '#2b2d2ee6', paper: '#2b2d2ee6' },
},
})
/**
* Represents the theme for the nav bar when it is dark
* @extends theme - The base theme
* @see [theme](../theme/theme.tsx)
*/
const navThemeDark = createTheme(theme, {
cssVariables: {cssVarPrefix: 'bt-nav-dark',},
components: {...},
palette: {
mode: 'dark',
secondary: { main: '#2B2D2E', dark: '#2B2D2E', light: '#626160', contrastText: '#fff' },
background: { default: '#ffffffe6', paper: '#ffffffe6' },
},
})
/**
* Hook to get the theme for the nav bar
* @param isDark to determine if the theme is dark or light
* @returns the theme for the nav bar
*/
export const useNavTheme = (isDark: boolean): Theme => {
const theme = useMemo(() => (isDark ? navThemeDark : navThemeLight), [isDark])
return theme
}
// src/app/layout.tsx
import { GlobalStyles, ThemeProvider } from '@mui/material'
import { Header } from '@/components/Header'
import { ErrorBoundary } from '@/components/helpers'
export default function RootLayout({ children }: LayoutBaseProps): React.JSX.Element {
return (
<html lang={LANGUAGE}>
<ThemeProvider theme={theme}>
<body className={Gotham.className}>
<GlobalStyles
styles={{
'@keyframes mui-auto-fill': { from: { display: 'block' } },
'@keyframes mui-auto-fill-cancel': { from: { display: 'block' } },
}}
/>
<ErrorBoundary fallback={null}>
<Header />
<main>{children}</main>
</ErrorBoundary>
</body>
</ThemeProvider>
</html>
)
}
// src/components/Header/index.tsx
'use client'
import { ThemeProvider } from '@mui/material'
import React, { type FC } from 'react'
import { useDevice } from '@/hooks/useDevice'
import { type ComponentBaseProps } from '@/interfaces/BaseProps'
import { useNavTheme } from '@/providers/theme/navTheme'
export const Header: FC<ComponentBaseProps> = ({ ... }) => {
const device = useDevice()
const isMobileOrTablet = device.isMobile || device.isTablet
const theme = useNavTheme(isMobileOrTablet)
return (
<ThemeProvider theme={theme}>
{/* More code */}
</ThemeProvider>
)
}
@jcohen14 @akattow thanks for your feedbacks, they are very helpful.
The extendTheme was added as an experimental API in v5. However, in v6 we learned that there should not be 2 APIs (createTheme and extendTheme) that do the same thing so we move the logic from extendTheme to createTheme.
To summarize, please replace extendTheme with createTheme (the parameters are likely the same except the CSS theme variables)
@jcohen14 As a design system team, you can provide different layers of the theme. For simplicity, you can provide an AppTheme that contains the default design system with a localization prop to change to language:
import { AppTheme } from 'lib/design-system'
function App() {
const locale = useLocalization(); // the white-labeled logic
return <AppTheme locale={locale}>
}
With the CSS theme variables feature, the white-labeled app can overrides the theme using plain CSS.
--mui-palette-primary-main: …
If the white-labeled app needs more control of the theme, you can provide lower-level tokens like:
import { themeTokens } from 'lib/design-system';
import { ThemeProvider, createTheme } from '@mui/material/styles';
function App() {
return <ThemeProvider theme={createTheme(themeTokens)}>
@akattow Awesome, we are using the same approach as your with the free templates.
I do have the same issue as you @JairTorres1003 Did you find a workaround?
@jcohen14 As a design system team, you can provide different layers of the theme. For simplicity, you can provide an
AppThemethat contains the default design system with a localization prop to change to language:
@siriwatknp This is a cool pattern but I don't think it applies to my use case.
Even after upgrading to v6 I'm still encountering the issue when trying to enable cssVariables in the theme.
The major benefit of my app's approach to building the theme is having access to utilities like theme.spacing and theme.shape when defining component themes. So we're not really creating multiple themes, just adding more and more to a single theme in steps. It looks a lot like this, but more extensive and across multiple files:
let theme = createTheme({
// tokens
});
theme = createTheme(theme, {
components: {
MuiButton: {
styleOverrides: {
root: {
padding: theme.spacing(4), // helper functions like this are a major benefit of this approach
backgroundColor: theme.palette.primary.main,
},
},
},
},
});
If this pattern is no longer being supported we can migrate, but the docs for createTheme are now really confusing in v6. Here it states that arguments past the first are basically ignored by createTheme but then in the very next section are examples where multiple arguments are being passed to it, implying it actually does care about arguments after the first.
@jcohen14 Gotcha. The approach you are using is not optimized because every time the createTheme is called, it's doing deepmerge between arguments. So if you are merging multiple times instead of one, it can be seen like this:
defaultTheme + { tokens }
defaultTheme + theme + { components: … }
I'd recommend creating the theme once and if you want to access theme utilities, use a callback at each styleOverrides slot instead:
{
components: {
MuiButton: {
styleOverrides: {
root: ({ theme }) => ({
padding: theme.spacing(4),
backgroundColor: theme.palette.primary.main,
}),
},
},
},
}
This way the { component: … } is independent from the theme creation.
The docs recommended using multiple createTheme calls to incrementally build the theme. What @siriwatknp recommend doesn't cover all use cases.
For example: Generate Tokens using Augment Color Utility
let theme = createTheme({ ...theme, cssVariables: true });
theme = createTheme(theme, {
palette: {
dark: theme.palette.augmentColor({
color: {
main: themeOptions.palette.dark.main
},
name: 'dark'
})
}
});
Turning on css variables like this will break your website.
Uncaught Error: MUI: `vars` is a private field used for CSS variables support.
Please use another name.
Workaround
A workaround I found is to only include the cssVariables on the last call to createTheme. By moving it to the last call, the bug doesn't happen. It only shows up after calling createTheme on a theme that already has the css variables enabled.
This is fixed by #45335
This issue has been closed. If you have a similar problem but not exactly the same, please open a new issue. Now, if you have additional information related to this issue or things that could help future readers, feel free to leave a comment.
[!NOTE] @jcohen14 How did we do? Your experience with our support team matters to us. If you have a moment, please share your thoughts in this short Support Satisfaction survey.