CSS theme variables
An overview of adopting CSS theme variables in Material UI or Joy UI.
CSS variables are a modern cross-browser feature that let you declare variables in CSS and reuse them in other properties.
Introduction
CSS theme variable support is a new feature in MUI System added in v5.0.5
as an experimental export. It tells the underlying Material UI, Joy UI or even custom UI library components to use the generated CSS theme variables instead of raw values. This provides significant improvements in developer experience related to theming and customization.
With these variables, you can inject a theme into your app's stylesheet at build time to apply the user's selected settings before the whole app is rendered.
You can checkout the advantages and trade-offs of using CSS theme variables before using them.
Advantages
- It lets you prevent dark-mode SSR flickering.
- You can create unlimited color schemes beyond
light
anddark
. - It offers a better debugging experience not only for developers but also designers on your team.
- The color scheme of your website is automatically synced between browser tabs.
- It simplifies integration with third-party tools because CSS theme variables are available globally.
- It reduces the need for a nested theme when you want to apply dark styles to a specific part of your application.
Trade-offs
For server-side applications, there are some trade-offs to consider:
Compare to the default method | Reason | |
---|---|---|
HTML size | Bigger | CSS variables are generated for both light and dark mode at build time. |
First Contentful Paint (FCP) | Larger | Since the HTML size is generally bigger, the time to download the HTML before showing the content is longer. |
Time to Interactive (TTI) | Smaller (for dark mode) | Stylesheets are not regenerated between light and dark mode, so it takes less time for JavaScript to run. |
Usage
The CSS variables API usage is exposed as a higher order function called unstable_createCssVarsProvider
which can be called to create a theme provider and other utitlities to share the theme config throughout your app. This is a very low-level function and has a lot of moving parts. If you are already using Material UI or Joy UI, they already expose their own CssVarsProvider
component that you can use directly without needing to configure your theme. Now that's out of the way, we can continue with how this util can be used.
We'll first define a minimal theme palette for light and dark modes.
// extendTheme.js
import {
unstable_createGetCssVar as systemCreateGetCssVar,
unstable_prepareCssVars as prepareCssVars,
} from '@mui/system';
const lightColorScheme = {
palette: {
mode: 'light',
primary: {
default: '#3990FF',
dark: '#02367D',
},
text: {
default: '#111111',
},
// ... other colors
},
};
const darkColorScheme = {
palette: {
mode: 'dark',
primary: {
default: '#265D97',
dark: '#132F4C',
main: '#5090D3',
},
text: {
default: '#ffffff',
},
// ... other colors
},
};
const createGetCssVar = (cssVarPrefix = 'my-app') =>
systemCreateGetCssVar(cssVarPrefix);
function extendTheme({ cssVarPrefix = 'my-app' } = {}) {
const getCssVar = createGetCssVar(cssVarPrefix);
const theme = {
colorSchemes: {
light: lightColorScheme,
dark: darkColorScheme,
},
// ... any other objects independent of color-scheme,
// like fontSizes, spacing tokens, etc
};
const { vars: themeVars, generateCssVars } = prepareCssVars(
{ colorSchemes: theme.colorSchemes },
{
prefix: cssVarPrefix,
},
);
theme.vars = themeVars;
theme.generateCssVars = generateCssVars;
theme.palette = {
...theme.colorSchemes.light.palette,
colorScheme: 'light',
};
return theme;
}
const myCustomDefaultTheme = extendTheme();
export default myCustomDefaultTheme;
Here, the returned theme
object needs to follow a certain structure to be used correctly by the final CssVarsProvider
. It should have a colorSchemes
key with the light and dark (and any other) palette. prepareCssVars
import from @mui/system
is used to create css variable names which can then be easily accessed using the returned vars
. This is also added to the theme
object. Finally, myCustomDefaultTheme
theme object is created that can now be passed to the createCssVarsProvider
to get a CssVarsProvider
.
// CssVarsProvider.js
import { unstable_createCssVarsProvider as createCssVarsProvider } from '@mui/system';
const { CssVarsProvider, useColorScheme } = createCssVarsProvider({
defaultColorScheme: {
light: 'light',
dark: 'dark',
},
theme: myCustomDefaultTheme,
});
export { CssVarsProvider, useColorScheme };
Now wrap your top level app component with this CssVarsProvider
component and then you can access the passed theme value to any of the components rendered inside the provider.
Example of a component using the css variable -
// Button.js
import { styled } from '@mui/system';
const Button = styled('button')(({ theme }) => ({
backgroundColor: theme.vars.palette.primary.default,
border: `1px solid ${theme.vars.palette.primary.dark}`,
color: theme.vars.palette.text.default,
}));
export default Button;
The hook, useColorScheme
can be used to get the current mode
(light or dark) and can also update the mode like:
// App.js
function App() {
const { setMode, mode } = useColorScheme();
const toggleMode = () => {
setMode(mode === 'dark' ? 'light' : 'dark');
};
return (
<div>
<h1>Current Mode: {mode}</h1>
<Button onClick={toggleMode}>Toggle Mode</Button>
</div>
);
}
// main.js
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import App from './App';
import { CssVarsProvider } from './CssVarsProvider';
ReactDOM.createRoot(document.getElementById('root')).render(
<CssVarsProvider>
<App />
</CssVarsProvider>,
);
Now, the Button's backgroundColor
, borderColor
and text color
values will correctly use the colors based on the selected mode
.
Demo
For framework or language specific setup, see this
See the complete usage of createVssVarsProvider
in Material UI and Joy UI.
API
createCssVarsProvider
options
attribute?
: DOM attribute for applying color scheme (data-color-scheme
by default)modeStorageKey?
: localStorage key used to store applicationmode
(mode
by default)colorSchemeStorageKey?
: localStorage key used to storecolorScheme
defaultColorScheme
: Design system default color scheme (string or object depending on if the design system has 1 or more themes, can belight
ordark
)defaultMode?
: Design system default mode (light
by default)disableTransitionOnChange?
: Disable CSS transitions when switching between modes or color schemes (false
by default)themeId?
: The design system's unique id for getting the corresponded theme when there are multiple design systems.theme
: Design system default theme. It's structure, besides the minimium requirements bycreateCssVarsProvider
, is upto the design system to implement.resolveTheme(theme: Theme) => Theme
: A function to be called after the CSS variables are attached. The result of this function will be the final theme pass toThemeProvider
.
createCssVarsProvider
returns 3 items.
<CssVarsProvider>
props
defaultMode?: 'light' | 'dark' | 'system'
- Application's default mode (light
by default)disableTransitionOnChange : boolean
- Disable CSS transitions when switching between modesenableColorScheme: boolean
- Indicate to the browser which color scheme is used (light or dark) for rendering built-in UIprefix: string
- CSS variable prefixtheme: ThemeInput
- the theme provided to React's contextmodeStorageKey?: string
- localStorage key used to store applicationmode
attribute?: string
- DOM attribute for applying color scheme
useColorScheme: () => ColorSchemeContextValue
mode: string
- The user's selected modesetMode: mode => {…}
- Function for setting themode
. Themode
is saved to internal state and local storage; ifmode
is null, it will be reset to the default mode
getInitColorSchemeScript: (options) => React.ReactElement
options
defaultMode?: 'light' | 'dark' | 'system'
: - Application's default mode before React renders the tree (light
by default)modeStorageKey?: string
: - localStorage key used to store applicationmode
attribute?: string
- DOM attribute for applying color scheme