@mui/material
Version:
Material UI is an open-source React component library that implements Google's Material Design. It's comprehensive and can be used in production out of the box.
513 lines (455 loc) • 14.3 kB
Markdown
# CSS theme variables - Native color
Learn how to use native color with CSS theme variables.
:::warning
This feature only works in modern browsers. Please check the [browser support](https://caniuse.com/css-relative-colors) before using it.
:::
## Benefits
- No need to use JavaScript to manipulate colors.
- Supports modern color spaces such as `oklch`, `oklab`, and `display-p3`.
- Supports color aliases to external CSS variables.
- Automatically calculates contrast text from the main color.
## Usage
Set `cssVariables` with `nativeColor: true` in the theme options.
Material UI will start using CSS color-mix and relative color instead of the JavaScript color manipulation.
:::success
Try inspecting the demo below to see the calculated values of the color tokens.
:::
```js
const theme = createTheme({
cssVariables: {
nativeColor: true,
},
});
```
```tsx
import { ThemeProvider, createTheme } from '/material/styles';
import Card from '/material/Card';
import CardContent from '/material/CardContent';
import CardActions from '/material/CardActions';
import Alert from '/material/Alert';
import Button from '/material/Button';
const theme = createTheme({
cssVariables: {
nativeColor: true,
cssVarPrefix: 'nativeColor', // This is for the demo only, you don't need to set this to use the feature
colorSchemeSelector: 'data-mui-color-scheme',
},
colorSchemes: {
light: true,
dark: true,
},
});
export default function NativeCssColors() {
return (
<ThemeProvider theme={theme}>
<Card>
<CardContent>
<Alert severity="info">
This theme uses the <code>oklch</code> color space.
</Alert>
</CardContent>
<CardActions sx={{ justifyContent: 'flex-end' }}>
<Button variant="contained" color="primary">
Submit
</Button>
<Button variant="outlined" color="primary">
Cancel
</Button>
</CardActions>
</Card>
</ThemeProvider>
);
}
```
## Modern color spaces
The theme palette supports all modern color spaces, including `oklch`, `oklab`, and `display-p3`.
```js
const theme = createTheme({
cssVariables: { nativeColor: true },
palette: {
primary: {
main: 'color(display-p3 0.5 0.8 0.2)',
},
},
});
```
```tsx
import * as React from 'react';
import { ThemeProvider, createTheme } from '/material/styles';
import Box from '/material/Box';
import Button from '/material/Button';
import Radio from '/material/Radio';
import RadioGroup from '/material/RadioGroup';
import FormLabel from '/material/FormLabel';
import FormControlLabel from '/material/FormControlLabel';
import FormControl from '/material/FormControl';
export default function ModernColorSpaces() {
const colorSpaces = [
'color(display-p3 0.7 0.5 0)', // Mud
'oklch(0.62 0.25 29)', // Orange
'oklab(0.59 0.1 -0.14)', // Purple
'hsl(141, 70%, 48%)', // Green
'rgb(25, 118, 210)', // Blue
];
const [selectedColor, setSelectedColor] = React.useState(colorSpaces[0]);
const theme = React.useMemo(
() =>
createTheme({
cssVariables: {
nativeColor: true,
cssVarPrefix: 'modern-color-spaces',
},
palette: {
primary: {
main: selectedColor,
},
},
}),
[selectedColor],
);
return (
<Box sx={{ display: 'flex', gap: 3, alignItems: 'center', flexWrap: 'wrap' }}>
<FormControl>
<FormLabel>Main color</FormLabel>
<RadioGroup
value={selectedColor}
onChange={(event) => setSelectedColor(event.target.value)}
>
{colorSpaces.map((value) => (
<FormControlLabel
key={value}
value={value}
control={<Radio />}
label={value}
/>
))}
</RadioGroup>
</FormControl>
<ThemeProvider theme={theme}>
<Button variant="contained" size="large">
Button
</Button>
</ThemeProvider>
</Box>
);
}
```
## Aliasing color variables
If you're using CSS variables to define colors, you can provide the values to the theme palette options.
```js
const theme = createTheme({
cssVariables: {
nativeColor: true,
},
palette: {
primary: {
main: 'var(--colors-brand-primary)',
},
},
});
```
```tsx
import { ThemeProvider, createTheme } from '/material/styles';
import GlobalStyles from '/material/GlobalStyles';
import Box from '/material/Box';
import Button from '/material/Button';
const theme = createTheme({
cssVariables: {
nativeColor: true,
cssVarPrefix: 'alias', // This is for the demo only, you don't need to set this to use the feature
},
palette: {
primary: {
main: 'var(--colors-brand-primary)',
},
},
});
export default function AliasColorVariables() {
return (
<div>
{/* This is just a demo to replicate the global CSS file */}
<GlobalStyles
styles={{
':root': {
'--colors-brand-primary': 'oklch(0.85 0.2 83.89)',
},
}}
/>
{/* Your App */}
<ThemeProvider theme={theme}>
<Box sx={{ p: 2 }}>
<Button variant="contained">Branded Button</Button>
</Box>
</ThemeProvider>
</div>
);
}
```
## Theme color functions
The theme object contains these color utilities: `alpha()`, `lighten()`, and `darken()`.
When native color is enabled, these functions use CSS `color-mix()` and relative color instead of the JavaScript color manipulation.
```tsx
import * as React from 'react';
import { ThemeProvider, createTheme } from '/material/styles';
import { blue, purple, red, green, orange, brown } from '/material/colors';
import Box from '/material/Box';
import Button from '/material/Button';
import Typography from '/material/Typography';
const theme = createTheme({
cssVariables: {
nativeColor: true,
// This is for the demo only, you don't need to set this to use the feature
cssVarPrefix: 'demo',
colorSchemeSelector: 'data',
},
colorSchemes: {
light: true,
dark: true,
},
});
const colorSwatches = [
{ color: blue[500] },
{ color: purple[500] },
{ color: red[500] },
{ color: brown[600] },
{ color: green[600] },
{ color: orange[500] },
];
function ColorDisplay({ color }: { color: string }) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Box
sx={{
width: 48,
height: 48,
bgcolor: color,
borderRadius: 1,
border: '1px solid',
borderColor: 'divider',
}}
/>
<Typography
variant="caption"
sx={{
color: 'text.secondary',
fontFamily: 'monospace',
wordBreak: 'break-all',
}}
>
{color}
</Typography>
</Box>
);
}
export default function ThemeColorFunctions() {
const [selectedColor, setSelectedColor] = React.useState(colorSwatches[0]);
const colorValues = {
alpha: theme.alpha(selectedColor.color, 0.5),
lighten: theme.lighten(selectedColor.color, 0.5),
darken: theme.darken(selectedColor.color, 0.5),
};
return (
<ThemeProvider theme={theme}>
<Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', gap: 1, mb: 3, flexWrap: 'wrap' }}>
{colorSwatches.map((swatch) => {
const isSelected = selectedColor.color === swatch.color;
return (
<Button
key={swatch.color}
variant={isSelected ? 'contained' : 'outlined'}
onClick={() => setSelectedColor(swatch)}
sx={(t) => ({
width: 56,
height: 56,
minWidth: 56,
p: 0,
fontSize: '0.625rem',
fontFamily: 'monospace',
borderColor: isSelected ? 'transparent' : swatch.color,
bgcolor: isSelected ? swatch.color : 'transparent',
color: isSelected
? t.palette.getContrastText(swatch.color)
: swatch.color,
})}
>
{swatch.color}
</Button>
);
})}
</Box>
<Box
sx={{
display: 'grid',
gap: 2,
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
}}
>
<div>
<Typography
variant="subtitle2"
gutterBottom
sx={{ fontWeight: 'medium' }}
>
theme.alpha(color, 0.5)
</Typography>
<ColorDisplay color={colorValues.alpha} />
</div>
<div>
<Typography
variant="subtitle2"
gutterBottom
sx={{ fontWeight: 'medium' }}
>
theme.lighten(color, 0.5)
</Typography>
<ColorDisplay color={colorValues.lighten} />
</div>
<div>
<Typography
variant="subtitle2"
gutterBottom
sx={{ fontWeight: 'medium' }}
>
theme.darken(color, 0.5)
</Typography>
<ColorDisplay color={colorValues.darken} />
</div>
</Box>
</Box>
</ThemeProvider>
);
}
```
:::info
The theme color functions are backward compatible.
If native color is not enabled, they will fall back to the JavaScript color manipulation.
:::
## Contrast color function
The `theme.palette.getContrastText()` function produces the contrast color.
The demo below shows the result of the `theme.palette.getContrastText()` function, which produces the text color based on the selected background.
```tsx
import * as React from 'react';
import { ThemeProvider, createTheme } from '/material/styles';
import Box from '/material/Box';
import Typography from '/material/Typography';
import Slider from '/material/Slider';
const theme = createTheme({
cssVariables: {
nativeColor: true,
cssVarPrefix: 'contrast', // This is for the demo only, you don't need to set this to use the feature
},
});
export default function ContrastTextDemo() {
const [lightness, setLightness] = React.useState(0.65);
const [chroma, setChroma] = React.useState(0.3);
const [hue, setHue] = React.useState(29);
// Create OKLCH color from slider values
const backgroundColor = `oklch(${lightness} ${chroma} ${hue})`;
// Get contrast text using theme function
const contrastText = theme.palette.getContrastText(backgroundColor);
return (
<ThemeProvider theme={theme}>
<Box
sx={{
p: 2,
display: 'flex',
gap: 5,
alignItems: 'flex-start',
justifyContent: 'center',
width: '100%',
flexWrap: 'wrap',
}}
>
{/* Live Preview Square */}
<Box
sx={{
mt: 2,
width: 200,
height: 200,
bgcolor: backgroundColor,
color: contrastText,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center',
borderRadius: 1,
border: '1px solid',
borderColor: 'divider',
flexShrink: 0,
}}
>
<Typography variant="body2" sx={{ fontFamily: 'monospace' }}>
{backgroundColor}
</Typography>
</Box>
{/* Sliders */}
<Box sx={{ flex: '1 1 300px', maxWidth: 400 }}>
<Typography variant="h6" gutterBottom>
OKLCH
</Typography>
<div>
<Typography variant="body2" gutterBottom>
Lightness: {lightness}
</Typography>
<Slider
value={lightness}
onChange={(_, value) => setLightness(value)}
min={0}
max={1}
step={0.01}
valueLabelDisplay="auto"
/>
</div>
<div>
<Typography variant="body2" gutterBottom>
Chroma: {chroma}
</Typography>
<Slider
value={chroma}
onChange={(_, value) => setChroma(value)}
min={0}
max={0.4}
step={0.01}
valueLabelDisplay="auto"
/>
</div>
<div>
<Typography variant="body2" gutterBottom>
Hue: {hue}°
</Typography>
<Slider
value={hue}
onChange={(_, value) => setHue(value)}
min={0}
max={360}
step={1}
valueLabelDisplay="auto"
/>
</div>
</Box>
<Box
sx={{
p: 2,
display: 'flex',
gap: 3,
}}
>
<Typography variant="body2" sx={{ fontFamily: 'monospace' }}>
<b>Text color:</b> {contrastText}
</Typography>
</Box>
</Box>
</ThemeProvider>
);
}
```
:::info
The CSS variables `--__l` and `--__a` are internal variables set globally by Material UI.
To learn more about the formulas used, see [this article on color contrast from Lea Verou](https://lea.verou.me/blog/2024/contrast-color/).
:::
## Caveats
- Because of the differences in how contrast is calculated between CSS and JavaScript, the resulting CSS colors may not exactly match the corresponding JavaScript colors to be replaced.
- In the future, the relative color contrast will be replaced by the native [CSS `contrast-color()` function](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/contrast-color) when browser support is improved.
- For relative color contrast, the color space is automatically set to `oklch` internally. Currently it's not possible to change this, but please [open an issue](https://github.com/mui/material-ui/issues/new/) if you have a use case that calls for it.