init project
Some checks failed
No response / noResponse (push) Has been cancelled
CI / Continuous releases (push) Has been cancelled
CI / test-dev (macos-latest) (push) Has been cancelled
CI / test-dev (ubuntu-latest) (push) Has been cancelled
CI / test-dev (windows-latest) (push) Has been cancelled
Maintenance / main (push) Has been cancelled
Scorecards supply-chain security / Scorecards analysis (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled

This commit is contained in:
how2ice
2025-12-12 14:26:25 +09:00
commit 005cf56baf
43188 changed files with 1079531 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
import { styled } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import { red, green, blue } from '@mui/material/colors';
const Root = styled('div')(({ theme }) => ({
padding: theme.spacing(1),
[theme.breakpoints.down('md')]: {
backgroundColor: red[500],
},
[theme.breakpoints.up('md')]: {
backgroundColor: blue[500],
},
[theme.breakpoints.up('lg')]: {
backgroundColor: green[500],
},
}));
export default function MediaQuery() {
return (
<Root>
<Typography>down(md): red</Typography>
<Typography>up(md): blue</Typography>
<Typography>up(lg): green</Typography>
</Root>
);
}

View File

@@ -0,0 +1,26 @@
import { styled } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import { red, green, blue } from '@mui/material/colors';
const Root = styled('div')(({ theme }) => ({
padding: theme.spacing(1),
[theme.breakpoints.down('md')]: {
backgroundColor: red[500],
},
[theme.breakpoints.up('md')]: {
backgroundColor: blue[500],
},
[theme.breakpoints.up('lg')]: {
backgroundColor: green[500],
},
}));
export default function MediaQuery() {
return (
<Root>
<Typography>down(md): red</Typography>
<Typography>up(md): blue</Typography>
<Typography>up(lg): green</Typography>
</Root>
);
}

View File

@@ -0,0 +1,5 @@
<Root>
<Typography>down(md): red</Typography>
<Typography>up(md): blue</Typography>
<Typography>up(lg): green</Typography>
</Root>

View File

@@ -0,0 +1,271 @@
# Breakpoints
<p class="description">API that enables the use of breakpoints in a wide variety of contexts.</p>
For optimal user experience, Material Design interfaces need to be able to adapt their layout at various breakpoints.
Material UI uses a **simplified** implementation of the original [specification](https://m2.material.io/design/layout/responsive-layout-grid.html#breakpoints).
The breakpoints are used internally in various components to make them responsive,
but you can also take advantage of them
for controlling the layout of your application through the [Grid](/material-ui/react-grid/) component.
## Default breakpoints
Each breakpoint (a key) matches with a _fixed_ screen width (a value):
<!-- Keep in sync with packages/mui-system/src/createTheme/createBreakpoints.d.ts -->
- **xs,** extra-small: 0px
- **sm,** small: 600px
- **md,** medium: 900px
- **lg,** large: 1200px
- **xl,** extra-large: 1536px
These values can be [customized](#custom-breakpoints).
## CSS Media Queries
CSS media queries are the idiomatic approach to make your UI responsive.
The theme provides five styles helpers to do so:
- [theme.breakpoints.up(key)](#theme-breakpoints-up-key-media-query)
- [theme.breakpoints.down(key)](#theme-breakpoints-down-key-media-query)
- [theme.breakpoints.only(key)](#theme-breakpoints-only-key-media-query)
- [theme.breakpoints.not(key)](#theme-breakpoints-not-key-media-query)
- [theme.breakpoints.between(start, end)](#theme-breakpoints-between-start-end-media-query)
In the following demo, we change the background color (red, blue & green) based on the screen width.
```jsx
const styles = (theme) => ({
root: {
padding: theme.spacing(1),
[theme.breakpoints.down('md')]: {
backgroundColor: theme.palette.secondary.main,
},
[theme.breakpoints.up('md')]: {
backgroundColor: theme.palette.primary.main,
},
[theme.breakpoints.up('lg')]: {
backgroundColor: green[500],
},
},
});
```
{{"demo": "MediaQuery.js"}}
## JavaScript Media Queries
Sometimes, using CSS isn't enough.
You might want to change the React rendering tree based on the breakpoint value, in JavaScript.
### useMediaQuery hook
You can learn more on the [useMediaQuery](/material-ui/react-use-media-query/) page.
## Custom breakpoints
You define your project's breakpoints in the `theme.breakpoints` section of your theme.
<!-- Keep in sync with packages/mui-system/src/createTheme/createBreakpoints.d.ts -->
- [`theme.breakpoints.values`](/material-ui/customization/default-theme/?expand-path=$.breakpoints.values): Default to the [above values](#default-breakpoints). The keys are your screen names, and the values are the min-width where that breakpoint should start.
- `theme.breakpoints.unit`: Default to `'px'`. The unit used for the breakpoint's values.
- `theme.breakpoints.step`: Default to `5`. The increment divided by 100 used to implement exclusive breakpoints.
For example, `{ step: 5 }` means that `down(500)` will result in `'(max-width: 499.95px)'`.
If you change the default breakpoints's values, you need to provide them all:
```jsx
const theme = createTheme({
breakpoints: {
values: {
xs: 0,
sm: 600,
md: 900,
lg: 1200,
xl: 1536,
},
},
});
```
Feel free to have as few or as many breakpoints as you want, naming them in whatever way you'd prefer for your project.
```js
const theme = createTheme({
breakpoints: {
values: {
mobile: 0,
tablet: 640,
laptop: 1024,
desktop: 1200,
},
},
});
```
If you are using TypeScript, you would also need to use [module augmentation](/material-ui/guides/typescript/#customization-of-theme) for the theme to accept the above values.
<!-- Tested with packages/mui-material/test/typescript/breakpointsOverrides.augmentation.tsconfig.json -->
```ts
declare module '@mui/material/styles' {
interface BreakpointOverrides {
xs: false; // removes the `xs` breakpoint
sm: false;
md: false;
lg: false;
xl: false;
mobile: true; // adds the `mobile` breakpoint
tablet: true;
laptop: true;
desktop: true;
}
}
```
## API
### `theme.breakpoints.up(key) => media query`
<!-- Keep in sync with packages/mui-system/src/createTheme/createBreakpoints.d.ts -->
#### Arguments
1. `key` (_string_ | _number_): A breakpoint key (`xs`, `sm`, etc.) or a screen width number in px.
#### Returns
`media query`: A media query string ready to be used with most styling solutions, which matches screen widths greater than the screen size given by the breakpoint key (inclusive).
#### Examples
```js
const styles = (theme) => ({
root: {
backgroundColor: 'blue',
// Match [md, ∞)
// [900px, ∞)
[theme.breakpoints.up('md')]: {
backgroundColor: 'red',
},
},
});
```
### `theme.breakpoints.down(key) => media query`
<!-- Keep in sync with packages/mui-system/src/createTheme/createBreakpoints.d.ts -->
#### Arguments
1. `key` (_string_ | _number_): A breakpoint key (`xs`, `sm`, etc.) or a screen width number in px.
#### Returns
`media query`: A media query string ready to be used with most styling solutions, which matches screen widths less than the screen size given by the breakpoint key (exclusive).
#### Examples
```js
const styles = (theme) => ({
root: {
backgroundColor: 'blue',
// Match [0, md)
// [0, 900px)
[theme.breakpoints.down('md')]: {
backgroundColor: 'red',
},
},
});
```
### `theme.breakpoints.only(key) => media query`
<!-- Keep in sync with packages/mui-system/src/createTheme/createBreakpoints.d.ts -->
#### Arguments
1. `key` (_string_): A breakpoint key (`xs`, `sm`, etc.).
#### Returns
`media query`: A media query string ready to be used with most styling solutions, which matches screen widths starting from the screen size given by the breakpoint key (inclusive) and stopping at the screen size given by the next breakpoint key (exclusive).
#### Examples
```js
const styles = (theme) => ({
root: {
backgroundColor: 'blue',
// Match [md, md + 1)
// [md, lg)
// [900px, 1200px)
[theme.breakpoints.only('md')]: {
backgroundColor: 'red',
},
},
});
```
### `theme.breakpoints.not(key) => media query`
<!-- Keep in sync with packages/mui-system/src/createTheme/createBreakpoints.d.ts -->
#### Arguments
1. `key` (_string_): A breakpoint key (`xs`, `sm`, etc.).
#### Returns
`media query`: A media query string ready to be used with most styling solutions, which matches screen widths stopping at the screen size given by the breakpoint key (exclusive) and starting at the screen size given by the next breakpoint key (inclusive).
#### Examples
```js
const styles = (theme) => ({
root: {
backgroundColor: 'blue',
// Match [xs, md) and [md + 1, ∞)
// [xs, md) and [lg, ∞)
// [0px, 900px) and [1200px, ∞)
[theme.breakpoints.not('md')]: {
backgroundColor: 'red',
},
},
});
```
### `theme.breakpoints.between(start, end) => media query`
<!-- Keep in sync with packages/mui-system/src/createTheme/createBreakpoints.d.ts -->
#### Arguments
1. `start` (_string_): A breakpoint key (`xs`, `sm`, etc.) or a screen width number in px.
2. `end` (_string_): A breakpoint key (`xs`, `sm`, etc.) or a screen width number in px.
#### Returns
`media query`: A media query string ready to be used with most styling solutions, which matches screen widths greater than the screen size given by the breakpoint key in the first argument (inclusive) and less than the screen size given by the breakpoint key in the second argument (exclusive).
#### Examples
```js
const styles = (theme) => ({
root: {
backgroundColor: 'blue',
// Match [sm, md)
// [600px, 900px)
[theme.breakpoints.between('sm', 'md')]: {
backgroundColor: 'red',
},
},
});
```
## Default values
You can explore the default values of the breakpoints using [the theme explorer](/material-ui/customization/default-theme/?expand-path=$.breakpoints) or by opening the dev tools console on this page (`window.theme.breakpoints`).

View File

@@ -0,0 +1,131 @@
import Box from '@mui/material/Box';
import { styled, useTheme } from '@mui/material/styles';
import * as colors from '@mui/material/colors';
const mainColors = [
'Red',
'Pink',
'Purple',
'Deep Purple',
'Indigo',
'Blue',
'Light Blue',
'Cyan',
'Teal',
'Green',
'Light Green',
'Lime',
'Yellow',
'Amber',
'Orange',
'Deep Orange',
'Brown',
'Grey',
'Blue Grey',
];
const mainPalette = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
const altPalette = ['A100', 'A200', 'A400', 'A700'];
const ColorGroup = styled('ul', { name: 'ColorGroup' })(({ theme }) => ({
padding: 0,
margin: theme.spacing(0, 2, 2, 0),
flexGrow: 1,
[theme.breakpoints.up('sm')]: {
flexGrow: 0,
width: '30%',
},
}));
const ColorValue = styled('span', { name: 'ColorValue' })(({ theme }) => ({
...theme.typography.caption,
color: 'inherit',
fontWeight: 'inherit',
}));
const ColorBlock = styled('li', { name: 'ColorBlock' })(
({ theme }) => theme.typography.body2,
);
function getColorBlock(theme, colorName, colorValue, colorTitle) {
const bgColor = colors[colorName][colorValue];
const fgColor = theme.palette.getContrastText(bgColor);
let blockTitle;
if (colorTitle) {
blockTitle = <Box sx={{ mb: '60px' }}>{colorName}</Box>;
}
let rowStyle = {
backgroundColor: bgColor,
color: fgColor,
listStyle: 'none',
padding: 15,
};
if (colorValue.toString().startsWith('A1')) {
rowStyle = {
...rowStyle,
marginTop: 4,
};
}
return (
<ColorBlock style={rowStyle} key={colorValue}>
{blockTitle}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<span>{colorValue}</span>
<ColorValue>{bgColor}</ColorValue>
</Box>
</ColorBlock>
);
}
function getColorGroup(options) {
const { theme, color, showAltPalette } = options;
const cssColor = color
.replace(' ', '')
.replace(color.charAt(0), color.charAt(0).toLowerCase());
let colorsList = [];
colorsList = mainPalette.map((mainValue) =>
getColorBlock(theme, cssColor, mainValue),
);
if (showAltPalette) {
altPalette.forEach((altValue) => {
colorsList.push(getColorBlock(theme, cssColor, altValue));
});
}
return (
<ColorGroup key={cssColor}>
{getColorBlock(theme, cssColor, 500, true)}
<Box sx={{ height: 4, listStyle: 'none' }} component="li" role="separator" />
{colorsList}
</ColorGroup>
);
}
function Color() {
const theme = useTheme();
return (
<Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
{mainColors.map((mainColor) =>
getColorGroup({
theme,
color: mainColor,
showAltPalette: true,
}),
)}
</Box>
);
}
export default Color;

View File

@@ -0,0 +1,88 @@
import PropTypes from 'prop-types';
import { useTheme } from '@mui/material/styles';
import Box from '@mui/material/Box';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Fab from '@mui/material/Fab';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import Typography from '@mui/material/Typography';
import AddIcon from '@mui/icons-material/Add';
function ColorDemo(props) {
const { data } = props;
const theme = useTheme();
const primary = theme.palette.augmentColor({
color: {
main: data.primary,
output:
data.primaryShade === 4
? `${data.primaryHue}`
: `{
main: '${data.primary}',
}`,
},
});
const secondary = theme.palette.augmentColor({
color: {
main: data.secondary,
output:
data.secondaryShade === 11
? `${data.secondaryHue}`
: `{
main: '${data.secondary}',
}`,
},
});
return (
<Box sx={{ position: 'relative', overflow: 'hidden' }}>
<Box sx={{ position: 'relative', height: 390, bgcolor: 'background.paper' }}>
<Box
sx={{ width: '100%', height: 24 }}
style={{ backgroundColor: primary.dark }}
/>
<AppBar position="static" style={{ backgroundColor: primary.main }}>
<Toolbar style={{ color: primary.contrastText }}>
<IconButton
edge="start"
sx={{ mr: '20px' }}
color="inherit"
aria-label="menu"
>
<MenuIcon />
</IconButton>
<Typography component="div" variant="h6" color="inherit">
Color
</Typography>
</Toolbar>
</AppBar>
<Box component="pre" sx={{ m: 2, fontSize: 16, overflowX: 'auto' }}>
{`{
palette: {
primary: ${primary.output},
secondary: ${secondary.output},
},
}`}
</Box>
<Fab
sx={{
position: 'absolute',
bottom: theme.spacing(2),
right: theme.spacing(2),
}}
style={{ backgroundColor: secondary.main }}
aria-label="add"
>
<AddIcon htmlColor={secondary.contrastText} />
</Fab>
</Box>
</Box>
);
}
ColorDemo.propTypes = {
data: PropTypes.object.isRequired,
};
export default ColorDemo;

View File

@@ -0,0 +1,319 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { rgbToHex, useTheme } from '@mui/material/styles';
import * as colors from '@mui/material/colors';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Input from '@mui/material/Input';
import Radio from '@mui/material/Radio';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import CheckIcon from '@mui/icons-material/Check';
import Slider from '@mui/material/Slider';
import { capitalize } from '@mui/material/utils';
import { resetDocsColor, setDocsColors } from 'docs/src/BrandingCssVarsProvider';
import ColorDemo from './ColorDemo';
const defaults = {
primary: '#2196f3',
secondary: '#f50057',
};
const hues = [
'red',
'pink',
'purple',
'deepPurple',
'indigo',
'blue',
'lightBlue',
'cyan',
'teal',
'green',
'lightGreen',
'lime',
'yellow',
'amber',
'orange',
'deepOrange',
];
const shades = [
900,
800,
700,
600,
500,
400,
300,
200,
100,
50,
'A700',
'A400',
'A200',
'A100',
];
const TooltipRadio = React.forwardRef(function TooltipRadio(props, ref) {
const {
'aria-labelledby': ariaLabelledBy,
'aria-label': ariaLabel,
inputProps,
...other
} = props;
return (
<Radio
ref={ref}
{...other}
inputProps={{
...inputProps,
'aria-labelledby': ariaLabelledBy,
'aria-label': ariaLabel,
}}
/>
);
});
TooltipRadio.propTypes = {
// possibly opaque identifier
'aria-label': PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
'aria-labelledby': PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
inputProps: PropTypes.object,
};
function ColorTool() {
const theme = useTheme();
const [state, setState] = React.useState({
primary: defaults.primary,
secondary: defaults.secondary,
primaryInput: defaults.primary,
secondaryInput: defaults.secondary,
primaryHue: 'blue',
secondaryHue: 'pink',
primaryShade: 4,
secondaryShade: 11,
});
const handleChangeColor = (name) => (event) => {
const isRgb = (string) =>
/rgb\([0-9]{1,3}\s*,\s*[0-9]{1,3}\s*,\s*[0-9]{1,3}\)/i.test(string);
const isHex = (string) => /^#?([0-9a-f]{3})$|^#?([0-9a-f]){6}$/i.test(string);
let {
target: { value: color },
} = event;
setState((prevState) => ({
...prevState,
[`${name}Input`]: color,
}));
let isValidColor = false;
if (isRgb(color)) {
isValidColor = true;
} else if (isHex(color)) {
isValidColor = true;
if (!color.includes('#')) {
color = `#${color}`;
}
}
if (isValidColor) {
setState((prevState) => ({
...prevState,
[name]: color,
}));
}
};
const handleChangeHue = (name) => (event) => {
const hue = event.target.value;
const color = colors[hue][shades[state[`${name}Shade`]]];
setState({
...state,
[`${name}Hue`]: hue,
[name]: color,
[`${name}Input`]: color,
});
};
const handleChangeShade = (name) => (event, shade) => {
const color = colors[state[`${name}Hue`]][shades[shade]];
setState({
...state,
[`${name}Shade`]: shade,
[name]: color,
[`${name}Input`]: color,
});
};
const handleChangeDocsColors = () => {
const paletteColors = {
primary: { ...colors[state.primaryHue], main: state.primary },
secondary: { ...colors[state.secondaryHue], main: state.secondary },
};
setDocsColors(paletteColors.primary, paletteColors.secondary);
document.cookie = `paletteColors=${JSON.stringify(
paletteColors,
)};path=/;max-age=31536000`;
};
const handleResetDocsColors = () => {
resetDocsColor();
document.cookie = 'paletteColors=;path=/;max-age=0';
};
const colorBar = (color) => {
const background = theme.palette.augmentColor({
color: {
main: color,
},
});
return (
<Grid container sx={{ mt: 2 }}>
{['dark', 'main', 'light'].map((key) => (
<Box
sx={{
width: 64,
height: 64,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
style={{ backgroundColor: background[key] }}
key={key}
>
<Typography
variant="caption"
style={{
color: theme.palette.getContrastText(background[key]),
}}
>
{rgbToHex(background[key])}
</Typography>
</Box>
))}
</Grid>
);
};
const colorPicker = (intent) => {
const intentInput = state[`${intent}Input`];
const intentShade = state[`${intent}Shade`];
const color = state[`${intent}`];
return (
<Grid
size={{
xs: 12,
sm: 6,
md: 4,
}}
>
<Typography component="label" gutterBottom htmlFor={intent} variant="h6">
{capitalize(intent)}
</Typography>
<Input
id={intent}
value={intentInput}
onChange={handleChangeColor(intent)}
fullWidth
/>
<Box sx={{ display: 'flex', alignItems: 'center', mt: 2, mb: 2 }}>
<Typography id={`${intent}ShadeSliderLabel`}>Shade:</Typography>
<Slider
sx={{ width: 'calc(100% - 80px)', ml: 3, mr: 3 }}
value={intentShade}
min={0}
max={13}
step={1}
onChange={handleChangeShade(intent)}
aria-labelledby={`${intent}ShadeSliderLabel`}
/>
<Typography minWidth={40}>{shades[intentShade]}</Typography>
</Box>
<Box sx={{ width: 192 }}>
{hues.map((hue) => {
const shade =
intent === 'primary'
? shades[state.primaryShade]
: shades[state.secondaryShade];
const backgroundColor = colors[hue][shade];
return (
<Tooltip placement="right" title={hue} key={hue} disableInteractive>
<TooltipRadio
sx={{ p: 0 }}
color="default"
checked={state[intent] === backgroundColor}
onChange={handleChangeHue(intent)}
value={hue}
name={intent}
icon={
<Box
sx={{ width: 48, height: 48 }}
style={{ backgroundColor }}
/>
}
checkedIcon={
<Box
sx={{
width: 48,
height: 48,
border: 1,
borderColor: 'white',
color: 'common.white',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
style={{ backgroundColor }}
>
<CheckIcon style={{ fontSize: 30 }} />
</Box>
}
/>
</Tooltip>
);
})}
</Box>
{colorBar(color)}
</Grid>
);
};
return (
<Grid container spacing={5} sx={{ p: 0 }}>
{colorPicker('primary')}
{colorPicker('secondary')}
<Grid
size={{
xs: 12,
sm: 6,
md: 4,
}}
>
<ColorDemo data={state} />
</Grid>
<Grid size={12}>
<Button variant="contained" onClick={handleChangeDocsColors}>
Set Docs Colors
</Button>
<Button variant="outlined" onClick={handleResetDocsColors} sx={{ ml: 1 }}>
Reset Docs Colors
</Button>
</Grid>
</Grid>
);
}
export default ColorTool;

View File

@@ -0,0 +1,119 @@
# Color
<p class="description">Convey meaning through color. Out of the box you get access to all colors in the Material Design guidelines.</p>
The Material Design [color system](https://m2.material.io/design/color/) can be used to create a color theme that reflects your brand or style.
## Picking colors
### Official color tool
The Material Design team has also built an awesome palette configuration tool: [material.io/resources/color/](https://m2.material.io/inline-tools/color/).
This can help you create a color palette for your UI, as well as measure the accessibility level of any color combination.
<a href="https://m2.material.io/inline-tools/color/" target="_blank" rel="noopener nofollow" class="remove-link-arrow">
<img src="/static/images/color/colorTool.png" alt="Official color tool" style="width: 574px" width=1148" height="610" />
</a>
<br />
<br />
The output can be fed into `createTheme()` function:
```js
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
palette: {
primary: {
light: '#757ce8',
main: '#3f50b5',
dark: '#002884',
contrastText: '#fff',
},
secondary: {
light: '#ff7961',
main: '#f44336',
dark: '#ba000d',
contrastText: '#000',
},
},
});
```
### Playground
To test a [material.io/design/color](https://m2.material.io/design/color/) color scheme with the Material UI documentation, simply select colors using the palette and sliders below.
Alternatively, you can enter hex values in the Primary and Secondary text fields.
{{"demo": "ColorTool.js", "hideToolbar": true, "bg": true}}
The output shown in the color sample can be pasted directly into a [`createTheme()`](/material-ui/customization/theming/#createtheme-options-args-theme) function (to be used with [`ThemeProvider`](/material-ui/customization/theming/#theme-provider)):
```jsx
import { createTheme } from '@mui/material/styles';
import { purple } from '@mui/material/colors';
const theme = createTheme({
palette: {
primary: {
main: purple[500],
},
secondary: {
main: '#f44336',
},
},
});
```
Only the `main` shades need to be provided (unless you wish to further customize `light`, `dark` or `contrastText`), as the other colors will be calculated by `createTheme()`, as described in the [Theme customization](/material-ui/customization/palette/) section.
If you are using the default primary and / or secondary shades then by providing the color object, `createTheme()` will use the appropriate shades from the material color for main, light and dark.
### Tools by the community
- [mui-theme-creator](https://zenoo.github.io/mui-theme-creator/): A tool to help design and customize themes for the Material UI component library. Includes basic site templates to show various components and how they are affected by the theme
- [Material palette generator](https://m2.material.io/inline-tools/color/): The Material palette generator can be used to generate a palette for any color you input.
## 2014 Material Design color palettes
These color palettes, originally created by Material Design in 2014, are comprised of colors designed to work together harmoniously, and can be used to develop your brand palette. To generate your own harmonious palettes, use the palette generation tool.
### Important Terms
- **Palette**: A palette is a collection of colors, that is hues and their shades. Material UI provides all colors from the Material Design guidelines.
[This color palette](#color-palette) has been designed with colors that work harmoniously with each other.
- **Hue & Shade**: A single color within the palette is made up of a hue such as "red", and shade, such as "500".
"red 50" is the lightest shade of red (_pink!_), while "red 900" is the darkest.
In addition, most hues come with "accent" shades, prefixed with an `A`.
### Color palette
Given a _HUE_ (red, pink, etc.) and a _SHADE_ (500, 600, etc.) you can import the color like this:
```jsx
import { red } from '@mui/material/colors';
const color = red[500];
```
{{"demo": "Color.js", "hideToolbar": true, "bg": "inline"}}
### Examples
For instance, you can refer to complementary primary and accent colors, "red 500" and "purple A200" like so:
```js
import { purple, red } from '@mui/material/colors';
const primary = red[500]; // #f44336
const accent = purple['A200']; // #e040fb
const accent = purple.A200; // #e040fb (alternative method)
```
### Accessibility
[WCAG 2.1 Rule 1.4.3](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html) does recommend
that you have a minimum of a 4.5:1 contrast ratio for the visual presentation of text and images of text.
Material UI currently only enforces a 3:1 contrast ratio. If you would like to meet WCAG 2.1 AA compliance,
you can increase your minimum contrast ratio as described in the
[Theme customization](/material-ui/customization/palette/#accessibility) section.

View File

@@ -0,0 +1,91 @@
import { styled } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Chip from '@mui/material/Chip';
import Typography from '@mui/material/Typography';
import ResizableDemo from './ResizableDemo';
const DynamicCard = styled(Card)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
[theme.containerQueries.up(350)]: {
flexDirection: 'row',
},
}));
const Image = styled('img')(({ theme }) => ({
alignSelf: 'stretch',
aspectRatio: '16 / 9',
objectFit: 'cover',
width: '100%',
maxHeight: 160,
transition: '0.4s',
[theme.containerQueries.up(350)]: {
maxWidth: '36%',
maxHeight: 'initial',
},
[theme.containerQueries.up(500)]: {
maxWidth: 240,
},
}));
const Content = styled(CardContent)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1),
padding: theme.spacing(2),
flex: 'auto',
transition: 'padding 0.4s',
[theme.containerQueries.up(500)]: {
padding: theme.spacing(3),
},
}));
export default function BasicContainerQueries() {
return (
<ResizableDemo>
<Box
sx={{
overflow: 'auto',
resize: 'horizontal',
width: 400,
maxWidth: 'min(80vw, 600px)',
containerType: 'inline-size', // required for container queries
}}
>
<DynamicCard variant="outlined">
<Image
alt="The house from the offer."
src="https://images.unsplash.com/photo-1512917774080-9991f1c4c750?auto=format&w=350&dpr=2"
/>
<Content>
<div>
<Typography
component="div"
sx={{ color: 'text.secondary', fontSize: '0.875rem' }}
>
123 Main St, Phoenix AZ
</Typography>
<Typography
component="div"
sx={{
color: 'primary.main',
fontSize: '1.125rem',
fontWeight: 'bold',
}}
>
$280,000 $310,000
</Typography>
</div>
<Chip
size="small"
label="Confidence score: 85%"
sx={{ p: 0, width: 'fit-content' }}
/>
</Content>
</DynamicCard>
</Box>
</ResizableDemo>
);
}

View File

@@ -0,0 +1,91 @@
import { styled } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Chip from '@mui/material/Chip';
import Typography from '@mui/material/Typography';
import ResizableDemo from './ResizableDemo';
const DynamicCard = styled(Card)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
[theme.containerQueries.up(350)]: {
flexDirection: 'row',
},
}));
const Image = styled('img')(({ theme }) => ({
alignSelf: 'stretch',
aspectRatio: '16 / 9',
objectFit: 'cover',
width: '100%',
maxHeight: 160,
transition: '0.4s',
[theme.containerQueries.up(350)]: {
maxWidth: '36%',
maxHeight: 'initial',
},
[theme.containerQueries.up(500)]: {
maxWidth: 240,
},
}));
const Content = styled(CardContent)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1),
padding: theme.spacing(2),
flex: 'auto',
transition: 'padding 0.4s',
[theme.containerQueries.up(500)]: {
padding: theme.spacing(3),
},
}));
export default function BasicContainerQueries() {
return (
<ResizableDemo>
<Box
sx={{
overflow: 'auto',
resize: 'horizontal',
width: 400,
maxWidth: 'min(80vw, 600px)',
containerType: 'inline-size', // required for container queries
}}
>
<DynamicCard variant="outlined">
<Image
alt="The house from the offer."
src="https://images.unsplash.com/photo-1512917774080-9991f1c4c750?auto=format&w=350&dpr=2"
/>
<Content>
<div>
<Typography
component="div"
sx={{ color: 'text.secondary', fontSize: '0.875rem' }}
>
123 Main St, Phoenix AZ
</Typography>
<Typography
component="div"
sx={{
color: 'primary.main',
fontSize: '1.125rem',
fontWeight: 'bold',
}}
>
$280,000 $310,000
</Typography>
</div>
<Chip
size="small"
label="Confidence score: 85%"
sx={{ p: 0, width: 'fit-content' }}
/>
</Content>
</DynamicCard>
</Box>
</ResizableDemo>
);
}

View File

@@ -0,0 +1,47 @@
import { styled } from '@mui/material/styles';
import Box from '@mui/material/Box';
const Line = styled('div')(({ theme }) => ({
position: 'absolute',
height: '100vh',
top: 0,
transform: 'translateY(-400px)',
left: 0,
borderLeft: '1px dashed',
borderColor: (theme.vars || theme).palette.divider,
color: (theme.vars || theme).palette.text.secondary,
fontSize: '0.75rem',
fontFamily: 'Menlo, monospace',
'& span': {
position: 'absolute',
top: 'calc(400px - 1em)',
left: 4,
},
}));
export default function ResizableDemo({ children }) {
return (
<Box
sx={{
position: 'relative',
paddingBlock: 2,
ml: 2,
mr: 'auto',
'*:has(> &)': {
overflow: 'hidden',
},
}}
>
<Line>
<span>0px</span>
</Line>
<Line sx={{ left: 350 }}>
<span>350px</span>
</Line>
<Line sx={{ left: 500 }}>
<span>500px</span>
</Line>
{children}
</Box>
);
}

View File

@@ -0,0 +1,91 @@
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Chip from '@mui/material/Chip';
import Typography from '@mui/material/Typography';
import ResizableDemo from './ResizableDemo';
export default function SxPropContainerQueries() {
return (
<ResizableDemo>
<Box
sx={{
overflow: 'auto',
resize: 'horizontal',
width: 400,
maxWidth: 'min(80vw, 600px)',
containerType: 'inline-size', // required for container queries
}}
>
<Card
variant="outlined"
sx={{
display: 'flex',
flexDirection: {
'@': 'column',
'@350': 'row',
},
}}
>
<Box
component="img"
alt="The house from the offer."
src="https://images.unsplash.com/photo-1512917774080-9991f1c4c750?auto=format&w=350&dpr=2"
sx={{
alignSelf: 'stretch',
aspectRatio: '16 / 9',
objectFit: 'cover',
width: '100%',
maxHeight: {
'@': 160,
'@350': 'initial',
},
maxWidth: {
'@350': '36%',
'@500': 240,
},
transition: '0.4s',
}}
/>
<CardContent
sx={{
display: 'flex',
flexDirection: 'column',
gap: 1,
padding: {
'@': 2,
'@500': 3,
},
flex: 'auto',
transition: 'padding 0.4s',
}}
>
<div>
<Typography
component="div"
sx={{ color: 'text.secondary', fontSize: '0.875rem' }}
>
123 Main St, Phoenix AZ
</Typography>
<Typography
component="div"
sx={{
color: 'primary.main',
fontSize: '1.125rem',
fontWeight: 'bold',
}}
>
$280,000 $310,000
</Typography>
</div>
<Chip
size="small"
label="Confidence score: 85%"
sx={{ p: 0, width: 'fit-content' }}
/>
</CardContent>
</Card>
</Box>
</ResizableDemo>
);
}

View File

@@ -0,0 +1,91 @@
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Chip from '@mui/material/Chip';
import Typography from '@mui/material/Typography';
import ResizableDemo from './ResizableDemo';
export default function SxPropContainerQueries() {
return (
<ResizableDemo>
<Box
sx={{
overflow: 'auto',
resize: 'horizontal',
width: 400,
maxWidth: 'min(80vw, 600px)',
containerType: 'inline-size', // required for container queries
}}
>
<Card
variant="outlined"
sx={{
display: 'flex',
flexDirection: {
'@': 'column',
'@350': 'row',
},
}}
>
<Box
component="img"
alt="The house from the offer."
src="https://images.unsplash.com/photo-1512917774080-9991f1c4c750?auto=format&w=350&dpr=2"
sx={{
alignSelf: 'stretch',
aspectRatio: '16 / 9',
objectFit: 'cover',
width: '100%',
maxHeight: {
'@': 160,
'@350': 'initial',
},
maxWidth: {
'@350': '36%',
'@500': 240,
},
transition: '0.4s',
}}
/>
<CardContent
sx={{
display: 'flex',
flexDirection: 'column',
gap: 1,
padding: {
'@': 2,
'@500': 3,
},
flex: 'auto',
transition: 'padding 0.4s',
}}
>
<div>
<Typography
component="div"
sx={{ color: 'text.secondary', fontSize: '0.875rem' }}
>
123 Main St, Phoenix AZ
</Typography>
<Typography
component="div"
sx={{
color: 'primary.main',
fontSize: '1.125rem',
fontWeight: 'bold',
}}
>
$280,000 $310,000
</Typography>
</div>
<Chip
size="small"
label="Confidence score: 85%"
sx={{ p: 0, width: 'fit-content' }}
/>
</CardContent>
</Card>
</Box>
</ResizableDemo>
);
}

View File

@@ -0,0 +1,72 @@
# Container queries
<p class="description">Material UI provides a utility function for creating CSS container queries based on theme breakpoints.</p>
## Usage
To create [CSS container queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries), use `theme.containerQueries` with any method available in the [`theme.breakpoints`](/material-ui/customization/breakpoints/#api).
The value can be unitless (in which case it'll be rendered in pixels), a string, or a breakpoint key. For example:
```js
theme.containerQueries.up('sm'); // => '@container (min-width: 600px)'
```
{{"demo": "BasicContainerQueries.js"}}
:::info
One of the ancestors must have the CSS container type specified.
:::
### Named containment contexts
To refer to a [containment context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries#naming_containment_contexts), call the `containerQueries` method with the name of the container for access to all breakpoint methods:
```js
theme.containerQueries('sidebar').up('500px'); // => '@container sidebar (min-width: 500px)'
```
## Shorthand syntax
When adding styles using the `sx` prop, use the `@<size>` or `@<size>/<name>` notation to apply container queries without referring to the theme.
- `<size>`: a width or a breakpoint key.
- `<name>` (optional): a named containment context.
{{"demo": "SxPropContainerQueries.js"}}
### Caveats
- The `@` prefix with a unitless value renders as `px`, so `@500` is equivalent to `500px`—but `@500px` is incorrect syntax and won't render correctly.
- `@` with no number renders as `0px`.
- Container queries must share the same units (the sizes can be defined in any order), as shown below:
```js
// ✅ These container queries will be sorted correctly.
padding: {
'@40em': 4,
'@20em': 2,
'@': 0,
}
// ❌ These container queries won't be sorted correctly
// because 40em is typically greater than 50px
// and the units don't match.
padding: {
'@40em': 4,
'@50': 2,
'@': 0,
}
```
## API
CSS container queries support all the methods available in [the breakpoints API](/material-ui/customization/breakpoints/#api).
```js
// For default breakpoints
theme.containerQueries.up('sm'); // => '@container (min-width: 600px)'
theme.containerQueries.down('md'); // => '@container (max-width: 900px)'
theme.containerQueries.only('md'); // => '@container (min-width: 600px) and (max-width: 900px)'
theme.containerQueries.between('sm', 'lg'); // => '@container (min-width: 600px) and (max-width: 1200px)'
theme.containerQueries.not('sm'); // => '@container (max-width: 600px)'
```

View File

@@ -0,0 +1,37 @@
import { styled } from '@mui/material/styles';
const StatRoot = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(0.5),
padding: theme.spacing(3, 4),
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[2],
letterSpacing: '-0.025em',
fontWeight: 600,
...theme.applyStyles('dark', {
backgroundColor: 'inherit',
}),
}));
const StatValue = styled('div')(({ theme }) => ({
...theme.typography.h3,
}));
const StatUnit = styled('div')(({ theme }) => ({
...theme.typography.body2,
color: theme.palette.text.secondary,
...theme.applyStyles('dark', {
color: 'inherit',
}),
}));
export default function StatComponent() {
return (
<StatRoot>
<StatValue>19,267</StatValue>
<StatUnit>Active users / month</StatUnit>
</StatRoot>
);
}

View File

@@ -0,0 +1,80 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import Stack from '@mui/material/Stack';
import { styled, useThemeProps } from '@mui/material/styles';
const StatRoot = styled('div', {
name: 'MuiStat',
slot: 'root',
})(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(0.5),
padding: theme.spacing(3, 4),
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[2],
letterSpacing: '-0.025em',
fontWeight: 600,
variants: [
{
props: {
variant: 'outlined',
},
style: {
border: `2px solid ${theme.palette.divider}`,
boxShadow: 'none',
},
},
],
...theme.applyStyles('dark', {
backgroundColor: 'inherit',
}),
}));
const StatValue = styled('div', {
name: 'MuiStat',
slot: 'value',
})(({ theme }) => ({
...theme.typography.h3,
}));
const StatUnit = styled('div', {
name: 'MuiStat',
slot: 'unit',
})(({ theme }) => ({
...theme.typography.body2,
color: theme.palette.text.secondary,
...theme.applyStyles('dark', {
color: 'inherit',
}),
}));
const Stat = React.forwardRef(function Stat(inProps, ref) {
const props = useThemeProps({ props: inProps, name: 'MuiStat' });
const { value, unit, variant, ...other } = props;
const ownerState = { ...props, variant };
return (
<StatRoot ref={ref} ownerState={ownerState} {...other}>
<StatValue ownerState={ownerState}>{value}</StatValue>
<StatUnit ownerState={ownerState}>{unit}</StatUnit>
</StatRoot>
);
});
Stat.propTypes = {
unit: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
variant: PropTypes.oneOf(['outlined']),
};
export default function StatFullTemplate() {
return (
<Stack direction="row" spacing={2}>
<Stat value="1.9M" unit="Favorites" />
<Stat value="5.1M" unit="Views" variant="outlined" />
</Stack>
);
}

View File

@@ -0,0 +1,86 @@
import * as React from 'react';
import Stack from '@mui/material/Stack';
import { styled, useThemeProps } from '@mui/material/styles';
export interface StatProps {
value: number | string;
unit: string;
variant?: 'outlined';
}
interface StatOwnerState extends StatProps {
// …key value pairs for the internal state that you want to style the slot
// but don't want to expose to the users
}
const StatRoot = styled('div', {
name: 'MuiStat',
slot: 'root',
})<{ ownerState: StatOwnerState }>(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(0.5),
padding: theme.spacing(3, 4),
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[2],
letterSpacing: '-0.025em',
fontWeight: 600,
variants: [
{
props: {
variant: 'outlined',
},
style: {
border: `2px solid ${theme.palette.divider}`,
boxShadow: 'none',
},
},
],
...theme.applyStyles('dark', {
backgroundColor: 'inherit',
}),
}));
const StatValue = styled('div', {
name: 'MuiStat',
slot: 'value',
})<{ ownerState: StatOwnerState }>(({ theme }) => ({
...theme.typography.h3,
}));
const StatUnit = styled('div', {
name: 'MuiStat',
slot: 'unit',
})<{ ownerState: StatOwnerState }>(({ theme }) => ({
...theme.typography.body2,
color: theme.palette.text.secondary,
...theme.applyStyles('dark', {
color: 'inherit',
}),
}));
const Stat = React.forwardRef<HTMLDivElement, StatProps>(
function Stat(inProps, ref) {
const props = useThemeProps({ props: inProps, name: 'MuiStat' });
const { value, unit, variant, ...other } = props;
const ownerState = { ...props, variant };
return (
<StatRoot ref={ref} ownerState={ownerState} {...other}>
<StatValue ownerState={ownerState}>{value}</StatValue>
<StatUnit ownerState={ownerState}>{unit}</StatUnit>
</StatRoot>
);
},
);
export default function StatFullTemplate() {
return (
<Stack direction="row" spacing={2}>
<Stat value="1.9M" unit="Favorites" />
<Stat value="5.1M" unit="Views" variant="outlined" />
</Stack>
);
}

View File

@@ -0,0 +1,2 @@
<Stat value="1.9M" unit="Favorites" />
<Stat value="5.1M" unit="Views" variant="outlined" />

View File

@@ -0,0 +1,57 @@
import { styled } from '@mui/material/styles';
const StatRoot = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(0.5),
padding: theme.spacing(3, 4),
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[2],
letterSpacing: '-0.025em',
fontWeight: 600,
...theme.applyStyles('dark', {
backgroundColor: 'inherit',
}),
}));
const StatValue = styled('div')(({ theme }) => ({
...theme.typography.h3,
}));
const StatUnit = styled('div')(({ theme }) => ({
...theme.typography.body2,
color: theme.palette.text.secondary,
...theme.applyStyles('dark', {
color: 'inherit',
}),
}));
const Label = styled('div')(({ theme }) => ({
borderRadius: '2px',
padding: theme.spacing(0, 1),
color: 'white',
position: 'absolute',
...theme.typography.body2,
fontSize: '0.75rem',
fontWeight: 500,
backgroundColor: '#ff5252',
}));
export default function StatSlots() {
return (
<StatRoot
sx={{ outline: '1px solid #ff5252', outlineOffset: 4, position: 'relative' }}
>
<StatValue sx={{ outline: '1px solid #ff5252', position: 'relative' }}>
19,267
<Label sx={{ right: 0, top: 4, transform: 'translateX(100%)' }}>value</Label>
</StatValue>
<StatUnit sx={{ outline: '1px solid #ff5252', position: 'relative' }}>
Active users / month
<Label sx={{ right: 0, top: 2, transform: 'translateX(100%)' }}>unit</Label>
</StatUnit>
<Label sx={{ left: -4, top: 4, transform: 'translateX(-100%)' }}>root</Label>
</StatRoot>
);
}

View File

@@ -0,0 +1,330 @@
# Creating themed components
<p class="description">Learn how to create fully custom components that accept your app's theme.</p>
## Introduction
Material UI provides a powerful theming feature that lets you add your own components to the theme and treat them as if they're built-in components.
If you are building a component library on top of Material UI, you can follow the step-by-step guide below to create a custom component that is themeable across multiple projects.
Alternatively, you can use the provided [template](#template) as a starting point for your component.
:::info
You don't need to connect your component to the theme if you are only using it in a single project.
:::
## Step-by-step guide
This guide will walk you through how to build this statistics component, which accepts the app's theme as though it were a built-in Material UI component:
{{"demo": "StatComponent.js", "hideToolbar": true}}
### 1. Create the component slots
Slots let you customize each individual element of the component by targeting its respective name in the [theme's styleOverrides](/material-ui/customization/theme-components/#theme-style-overrides) and [theme's variants](/material-ui/customization/theme-components/#variants).
This statistics component is composed of three slots:
- `root`: the container of the component
- `value`: the number of the statistics
- `unit`: the unit or description of the statistics
:::success
Though you can give these slots any names you prefer, we recommend using `root` for the outermost container element for consistency with the rest of the library.
:::
{{"demo": "StatSlots.js", "hideToolbar": true}}
Use the `styled` API with `name` and `slot` parameters to create the slots, as shown below:
```js
import * as React from 'react';
import { styled } from '@mui/material/styles';
const StatRoot = styled('div', {
name: 'MuiStat', // The component name
slot: 'root', // The slot name
})(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(0.5),
padding: theme.spacing(3, 4),
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[2],
letterSpacing: '-0.025em',
fontWeight: 600,
...theme.applyStyles('dark', {
backgroundColor: 'inherit',
}),
}));
const StatValue = styled('div', {
name: 'MuiStat',
slot: 'value',
})(({ theme }) => ({
...theme.typography.h3,
}));
const StatUnit = styled('div', {
name: 'MuiStat',
slot: 'unit',
})(({ theme }) => ({
...theme.typography.body2,
color: theme.palette.text.secondary,
}));
```
### 2. Create the component
Assemble the component using the slots created in the previous step:
```js
// /path/to/Stat.js
import * as React from 'react';
const StatRoot = styled('div', {
name: 'MuiStat',
slot: 'root',
})();
const StatValue = styled('div', {
name: 'MuiStat',
slot: 'value',
})();
const StatUnit = styled('div', {
name: 'MuiStat',
slot: 'unit',
})();
const Stat = React.forwardRef(function Stat(props, ref) {
const { value, unit, ...other } = props;
return (
<StatRoot ref={ref} {...other}>
<StatValue>{value}</StatValue>
<StatUnit>{unit}</StatUnit>
</StatRoot>
);
});
export default Stat;
```
At this point, you'll be able to apply the theme to the `Stat` component like this:
```js
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
components: {
// the component name defined in the `name` parameter
// of the `styled` API
MuiStat: {
styleOverrides: {
// the slot name defined in the `slot` and `overridesResolver` parameters
// of the `styled` API
root: {
backgroundColor: '#121212',
},
value: {
color: '#fff',
},
unit: {
color: '#888',
},
},
},
},
});
```
### 3. Style the slot with ownerState
When you need to style the slot-based props or internal state, wrap them in the `ownerState` object and pass it to each slot as a prop.
The `ownerState` is a special name that will not spread to the DOM via the `styled` API.
Add a `variant` prop to the `Stat` component and use it to style the `root` slot, as shown below:
```diff
const Stat = React.forwardRef(function Stat(props, ref) {
+ const { value, unit, variant, ...other } = props;
+
+ const ownerState = { ...props, variant };
return (
- <StatRoot ref={ref} {...other}>
- <StatValue>{value}</StatValue>
- <StatUnit>{unit}</StatUnit>
- </StatRoot>
+ <StatRoot ref={ref} ownerState={ownerState} {...other}>
+ <StatValue ownerState={ownerState}>{value}</StatValue>
+ <StatUnit ownerState={ownerState}>{unit}</StatUnit>
+ </StatRoot>
);
});
```
Then you can read `ownerState` in the slot to style it based on the `variant` prop.
```diff
const StatRoot = styled('div', {
name: 'MuiStat',
slot: 'root',
- })(({ theme }) => ({
+ })(({ theme, ownerState }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(0.5),
padding: theme.spacing(3, 4),
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[2],
letterSpacing: '-0.025em',
fontWeight: 600,
...theme.applyStyles('dark', {
backgroundColor: 'inherit',
}),
+ ...ownerState.variant === 'outlined' && {
+ border: `2px solid ${theme.palette.divider}`,
+ },
}));
```
### 4. Support theme default props
To customize your component's default props for different projects, you need to use the `useThemeProps` API.
```diff
+ import { useThemeProps } from '@mui/material/styles';
- const Stat = React.forwardRef(function Stat(props, ref) {
+ const Stat = React.forwardRef(function Stat(inProps, ref) {
+ const props = useThemeProps({ props: inProps, name: 'MuiStat' });
const { value, unit, ...other } = props;
return (
<StatRoot ref={ref} {...other}>
<StatValue>{value}</StatValue>
<StatUnit>{unit}</StatUnit>
</StatRoot>
);
});
```
Then you can customize the default props of your component like this:
```js
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
components: {
MuiStat: {
defaultProps: {
variant: 'outlined',
},
},
},
});
```
## TypeScript
If you use TypeScript, you must create interfaces for the component props and ownerState:
```js
interface StatProps {
value: number | string;
unit: string;
variant?: 'outlined';
}
interface StatOwnerState extends StatProps {
// …key value pairs for the internal state that you want to style the slot
// but don't want to expose to the users
}
```
Then you can use them in the component and slots.
```js
const StatRoot = styled('div', {
name: 'MuiStat',
slot: 'root',
})<{ ownerState: StatOwnerState }>(({ theme, ownerState }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(0.5),
padding: theme.spacing(3, 4),
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[2],
letterSpacing: '-0.025em',
fontWeight: 600,
...theme.applyStyles('dark', {
backgroundColor: 'inherit',
}),
// typed-safe access to the `variant` prop
...(ownerState.variant === 'outlined' && {
border: `2px solid ${theme.palette.divider}`,
boxShadow: 'none',
}),
}));
// …do the same for other slots
const Stat = React.forwardRef<HTMLDivElement, StatProps>(function Stat(inProps, ref) {
const props = useThemeProps({ props: inProps, name: 'MuiStat' });
const { value, unit, variant, ...other } = props;
const ownerState = { ...props, variant };
return (
<StatRoot ref={ref} ownerState={ownerState} {...other}>
<StatValue ownerState={ownerState}>{value}</StatValue>
<StatUnit ownerState={ownerState}>{unit}</StatUnit>
</StatRoot>
);
});
```
Finally, add the Stat component to the theme types.
```ts
import {
ComponentsOverrides,
ComponentsVariants,
Theme as MuiTheme,
} from '@mui/material/styles';
import { StatProps } from 'path/to/Stat';
type Theme = Omit<MuiTheme, 'components'>;
declare module '@mui/material/styles' {
interface ComponentNameToClassKey {
MuiStat: 'root' | 'value' | 'unit';
}
interface ComponentsPropsList {
MuiStat: Partial<StatProps>;
}
interface Components {
MuiStat?: {
defaultProps?: ComponentsPropsList['MuiStat'];
styleOverrides?: ComponentsOverrides<Theme>['MuiStat'];
variants?: ComponentsVariants['MuiStat'];
};
}
}
```
---
## Template
This template is the final product of the step-by-step guide above, demonstrating how to build a custom component that can be styled with the theme as if it was a built-in component.
{{"demo": "StatFullTemplate.js", "defaultCodeOpen": true}}

View File

@@ -0,0 +1,84 @@
import * as React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import Accordion from '@mui/material/Accordion';
import AccordionSummary from '@mui/material/AccordionSummary';
import AccordionDetails from '@mui/material/AccordionDetails';
import Typography from '@mui/material/Typography';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import Box from '@mui/material/Box';
import Switch from '@mui/material/Switch';
export default function CssLayersCaveat() {
const [cssLayers, setCssLayers] = React.useState(false);
const theme = React.useMemo(() => {
return createTheme({
modularCssLayers: cssLayers,
cssVariables: true,
components: {
MuiAccordion: {
styleOverrides: {
root: {
margin: 0,
},
},
},
},
});
}, [cssLayers]);
return (
<div>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '16px',
}}
>
<Typography
component="span"
sx={{ marginRight: '8px', fontSize: '14px', color: 'text.secondary' }}
>
No CSS Layers
</Typography>
<Switch checked={cssLayers} onChange={() => setCssLayers(!cssLayers)} />
<Typography
component="span"
sx={{ marginLeft: '8px', fontSize: '14px', color: 'text.secondary' }}
>
With CSS Layers
</Typography>
</Box>
<ThemeProvider theme={theme}>
<div>
<Accordion defaultExpanded>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1-content"
id="panel1-header"
>
<Typography component="span">Accordion 1</Typography>
</AccordionSummary>
<AccordionDetails>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</AccordionDetails>
</Accordion>
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel2-content"
id="panel2-header"
>
<Typography component="span">Accordion 2</Typography>
</AccordionSummary>
<AccordionDetails>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</AccordionDetails>
</Accordion>
</div>
</ThemeProvider>
</div>
);
}

View File

@@ -0,0 +1,84 @@
import * as React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import Accordion from '@mui/material/Accordion';
import AccordionSummary from '@mui/material/AccordionSummary';
import AccordionDetails from '@mui/material/AccordionDetails';
import Typography from '@mui/material/Typography';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import Box from '@mui/material/Box';
import Switch from '@mui/material/Switch';
export default function CssLayersCaveat() {
const [cssLayers, setCssLayers] = React.useState(false);
const theme = React.useMemo(() => {
return createTheme({
modularCssLayers: cssLayers,
cssVariables: true,
components: {
MuiAccordion: {
styleOverrides: {
root: {
margin: 0,
},
},
},
},
});
}, [cssLayers]);
return (
<div>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '16px',
}}
>
<Typography
component="span"
sx={{ marginRight: '8px', fontSize: '14px', color: 'text.secondary' }}
>
No CSS Layers
</Typography>
<Switch checked={cssLayers} onChange={() => setCssLayers(!cssLayers)} />
<Typography
component="span"
sx={{ marginLeft: '8px', fontSize: '14px', color: 'text.secondary' }}
>
With CSS Layers
</Typography>
</Box>
<ThemeProvider theme={theme}>
<div>
<Accordion defaultExpanded>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1-content"
id="panel1-header"
>
<Typography component="span">Accordion 1</Typography>
</AccordionSummary>
<AccordionDetails>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</AccordionDetails>
</Accordion>
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel2-content"
id="panel2-header"
>
<Typography component="span">Accordion 2</Typography>
</AccordionSummary>
<AccordionDetails>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</AccordionDetails>
</Accordion>
</div>
</ThemeProvider>
</div>
);
}

View File

@@ -0,0 +1,43 @@
import { createTheme, ThemeProvider } from '@mui/material/styles';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import FormHelperText from '@mui/material/FormHelperText';
const theme = createTheme({
modularCssLayers: true,
cssVariables: true,
});
export default function CssLayersInput() {
return (
<ThemeProvider theme={theme}>
<FormControl variant="outlined">
<InputLabel
shrink
htmlFor="css-layers-input"
sx={{
width: 'fit-content',
transform: 'none',
position: 'relative',
mb: 0.25,
fontWeight: 'medium',
pointerEvents: 'auto',
}}
>
Label
</InputLabel>
<OutlinedInput
id="css-layers-input"
placeholder="Type something"
slotProps={{
input: {
sx: { py: 1.5, height: '2.5rem', boxSizing: 'border-box' },
},
}}
/>
<FormHelperText sx={{ marginLeft: 0 }}>Helper text goes here</FormHelperText>
</FormControl>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,43 @@
import { createTheme, ThemeProvider } from '@mui/material/styles';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import FormHelperText from '@mui/material/FormHelperText';
const theme = createTheme({
modularCssLayers: true,
cssVariables: true,
});
export default function CssLayersInput() {
return (
<ThemeProvider theme={theme}>
<FormControl variant="outlined">
<InputLabel
shrink
htmlFor="css-layers-input"
sx={{
width: 'fit-content',
transform: 'none',
position: 'relative',
mb: 0.25,
fontWeight: 'medium',
pointerEvents: 'auto',
}}
>
Label
</InputLabel>
<OutlinedInput
id="css-layers-input"
placeholder="Type something"
slotProps={{
input: {
sx: { py: 1.5, height: '2.5rem', boxSizing: 'border-box' },
},
}}
/>
<FormHelperText sx={{ marginLeft: 0 }}>Helper text goes here</FormHelperText>
</FormControl>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,287 @@
# CSS Layers
<p class="description">Learn how to generate Material UI styles with cascade layers.</p>
## What are cascade layers?
Cascade layers are an advanced CSS feature that make it possible to control the order in which styles are applied to elements.
If you're not familiar with cascade layers, visit the [MDN documentation](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Cascade_layers) for a detailed overview.
Benefits of using cascade layers include:
- **Improved specificity**: Cascade layers let you control the order of the styles, which can help avoid specificity conflicts. For example, you can theme a component without hitting the default specificity of the styles.
- **Better integration with CSS frameworks**: With cascade layers, you can use Tailwind CSS v4 utility classes to override Material UI styles without the need for the `!important` directive.
- **Better debuggability**: Cascade layers appear in the browser's dev tools, making it easier to see which styles are applied and in what order.
## Implementing a single cascade layer
This method creates a single layer, namely `@layer mui`, for all Material UI components and global styles.
This is suitable for integrating with other styling solutions, such as Tailwind CSS v4, that use the `@layer` directive.
### Next.js App Router
Start by configuring Material UI with Next.js in the [App Router integration guide](/material-ui/integrations/nextjs/#app-router).
Then follow these steps:
1. Enable the [CSS layer feature](/material-ui/integrations/nextjs/#using-other-styling-solutions) in the root layout:
```tsx title="src/app/layout.tsx"
import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
export default function RootLayout() {
return (
<html lang="en" suppressHydrationWarning>
<body>
<AppRouterCacheProvider options={{ enableCssLayer: true }}>
{/* Your app */}
</AppRouterCacheProvider>
</body>
</html>
);
}
```
2. Configure the layer order at the top of a CSS file to work with Tailwind CSS v4:
```css title="src/app/globals.css"
@layer theme, base, mui, components, utilities;
```
### Next.js Pages Router
Start by configuring Material UI with Next.js in the [Pages Router integration guide](/material-ui/integrations/nextjs/#pages-router).
Then follow these steps:
1. Enable the [CSS layer feature](/material-ui/integrations/nextjs/#configuration-2) in a custom `_document`:
```tsx title="pages/_document.tsx"
import {
createCache,
documentGetInitialProps,
} from '@mui/material-nextjs/v15-pagesRouter';
// ...
MyDocument.getInitialProps = async (ctx: DocumentContext) => {
const finalProps = await documentGetInitialProps(ctx, {
emotionCache: createCache({ enableCssLayer: true }),
});
return finalProps;
};
```
2. Configure the layer order with the `GlobalStyles` component to work with Tailwind CSS v4—it must be the first child of the `AppCacheProvider`:
```tsx title="pages/_app.tsx"
import { AppCacheProvider } from '@mui/material-nextjs/v15-pagesRouter';
import GlobalStyles from '@mui/material/GlobalStyles';
export default function MyApp(props: AppProps) {
const { Component, pageProps } = props;
return (
<AppCacheProvider {...props}>
<GlobalStyles styles="@layer theme, base, mui, components, utilities;" />
<Component {...pageProps} />
</AppCacheProvider>
);
}
```
### Vite or any other SPA
Make the following changes in `src/main.tsx`:
1. Pass the `enableCssLayer` prop to the `StyledEngineProvider` component.
2. Configure the layer order with the `GlobalStyles` component to work with Tailwind CSS v4.
```tsx title="main.tsx"
import { StyledEngineProvider } from '@mui/material/styles';
import GlobalStyles from '@mui/material/GlobalStyles';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<StyledEngineProvider enableCssLayer>
<GlobalStyles styles="@layer theme, base, mui, components, utilities;" />
{/* Your app */}
</StyledEngineProvider>
</React.StrictMode>,
);
```
## Implementing multiple cascade layers
After you've set up a [single cascade layer](#implementing-a-single-cascade-layer), you can split the styles into multiple layers to better organize them within Material UI.
This makes it simpler to apply theming and override styles with the `sx` prop.
First, follow the steps from the [previous section](#implementing-a-single-cascade-layer) to enable the CSS layer feature.
Then, create a new file and export the component that wraps the `ThemeProvider` from Material UI.
Finally, pass the `modularCssLayers: true` option to the `createTheme` function:
```tsx title="src/theme.tsx"
import { createTheme, ThemeProvider } from '@mui/material/styles';
const theme = createTheme({
modularCssLayers: true,
});
export default function AppTheme({ children }: { children: ReactNode }) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
```
{{"demo": "CssLayersInput.js"}}
When this feature is enabled, Material UI generates these layers:
- `@layer mui.global`: Global styles from the `GlobalStyles` and `CssBaseline` components.
- `@layer mui.components`: Base styles for all Material UI components.
- `@layer mui.theme`: Theme styles for all Material UI components.
- `@layer mui.custom`: Custom styles for non-Material UI styled components.
- `@layer mui.sx`: Styles from the `sx` prop.
The sections below demonstrate how to set up multiple cascade layers for Material UI with common React frameworks.
### Next.js App Router
```tsx title="src/theme.tsx"
'use client';
import React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
const theme = createTheme({
modularCssLayers: true,
});
export default function AppTheme({ children }: { children: React.ReactNode }) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
```
```tsx title="src/app/layout.tsx"
import AppTheme from '../theme';
export default function RootLayout() {
return (
<html lang="en" suppressHydrationWarning>
<body>
<AppRouterCacheProvider options={{ enableCssLayer: true }}>
<AppTheme>{/* Your app */}</AppTheme>
</AppRouterCacheProvider>
</body>
</html>
);
}
```
### Next.js Pages Router
```tsx title="src/theme.tsx"
import { createTheme, ThemeProvider } from '@mui/material/styles';
const theme = createTheme({
modularCssLayers: true,
});
export default function AppTheme({ children }: { children: ReactNode }) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
```
```tsx title="pages/_app.tsx"
import AppTheme from '../src/theme';
export default function MyApp(props: AppProps) {
const { Component, pageProps } = props;
return (
<AppCacheProvider {...props}>
<AppTheme>
<Component {...pageProps} />
</AppTheme>
</AppCacheProvider>
);
}
```
```tsx title="pages/_document.tsx"
import {
createCache,
documentGetInitialProps,
} from '@mui/material-nextjs/v15-pagesRouter';
MyDocument.getInitialProps = async (ctx: DocumentContext) => {
const finalProps = await documentGetInitialProps(ctx, {
emotionCache: createCache({ enableCssLayer: true }),
});
return finalProps;
};
```
### Vite or any other SPA
```tsx title="src/theme.tsx"
import { createTheme, ThemeProvider } from '@mui/material/styles';
const theme = createTheme({
modularCssLayers: true,
});
export default function AppTheme({ children }: { children: ReactNode }) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
```
```tsx title="src/main.tsx"
import AppTheme from './theme';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<StyledEngineProvider enableCssLayer>
<AppTheme>{/* Your app */}</AppTheme>
</StyledEngineProvider>
</React.StrictMode>,
);
```
### Usage with other styling solutions
To integrate with other styling solutions, such as Tailwind CSS v4, replace the boolean value for `modularCssLayers` with a string specifying the layer order.
Material UI will look for the `mui` identifier and generate the layers in the correct order:
```diff title="src/theme.tsx"
const theme = createTheme({
- modularCssLayers: true,
+ modularCssLayers: '@layer theme, base, mui, components, utilities;',
});
```
The generated CSS will look like this:
```css
@layer theme, base, mui.global, mui.components, mui.theme, mui.custom, mui.sx, components, utilities;
```
### Caveats
If you enable `modularCssLayers` in an app that already has custom styles and theme overrides applied to it, you may observe unexpected changes to the look and feel of the UI due to the differences in specificity before and after.
For example, if you have the following [theme style overrides](/material-ui/customization/theme-components/#theme-style-overrides) for the [Accordion](/material-ui/react-accordion/) component:
```js
const theme = createTheme({
components: {
MuiAccordion: {
styleOverrides: {
root: {
margin: 0,
},
},
},
},
});
```
By default, the margin from the theme does _not_ take precedence over the default margin styles when the accordion is expanded, because it has higher specificity than the theme styles—so this code has no effect.
After enabling the `modularCssLayers` option, the margin from the theme _does_ take precedence because the theme layer comes after the components layer in the cascade order—so the style override is applied and the accordion has no margins when expanded.
{{"demo": "CssLayersCaveat.js"}}

View File

@@ -0,0 +1,37 @@
import { ThemeProvider, createTheme } from '@mui/material/styles';
import GlobalStyles from '@mui/material/GlobalStyles';
import Box from '@mui/material/Box';
import Button from '@mui/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>
);
}

View File

@@ -0,0 +1,38 @@
import { ThemeProvider, createTheme } from '@mui/material/styles';
import GlobalStyles from '@mui/material/GlobalStyles';
import Box from '@mui/material/Box';
import Button from '@mui/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>
);
}

View File

@@ -0,0 +1,15 @@
{/* 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>

View File

@@ -0,0 +1,122 @@
import * as React from 'react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Slider from '@mui/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" 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" fontFamily="monospace">
<b>Text color:</b> {contrastText}
</Typography>
</Box>
</Box>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,122 @@
import * as React from 'react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Slider from '@mui/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" 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" fontFamily="monospace">
<b>Text color:</b> {contrastText}
</Typography>
</Box>
</Box>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,43 @@
import { ThemeProvider, createTheme } from '@mui/material/styles';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions';
import Alert from '@mui/material/Alert';
import Button from '@mui/material/Button';
const theme = createTheme({
cssVariables: {
nativeColor: true,
cssVarPrefix: 'colorSpace', // This is for the demo only, you don't need to set this to use the feature
},
palette: {
primary: {
main: 'oklch(0.65 0.3 28.95)',
},
warning: {
main: 'oklch(0.72 0.24 44.32)',
},
},
});
export default function CustomColorSpace() {
return (
<ThemeProvider theme={theme}>
<Card>
<CardContent>
<Alert severity="info" color="warning">
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>
);
}

View File

@@ -0,0 +1,43 @@
import { ThemeProvider, createTheme } from '@mui/material/styles';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions';
import Alert from '@mui/material/Alert';
import Button from '@mui/material/Button';
const theme = createTheme({
cssVariables: {
nativeColor: true,
cssVarPrefix: 'colorSpace', // This is for the demo only, you don't need to set this to use the feature
},
palette: {
primary: {
main: 'oklch(0.65 0.3 28.95)',
},
warning: {
main: 'oklch(0.72 0.24 44.32)',
},
},
});
export default function CustomColorSpace() {
return (
<ThemeProvider theme={theme}>
<Card>
<CardContent>
<Alert severity="info" color="warning">
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>
);
}

View File

@@ -0,0 +1,69 @@
import * as React from 'react';
import { createTheme, ThemeProvider, useColorScheme } from '@mui/material/styles';
import Stack from '@mui/material/Stack';
import MenuItem from '@mui/material/MenuItem';
import Switch from '@mui/material/Switch';
import Select from '@mui/material/Select';
import FormControlLabel from '@mui/material/FormControlLabel';
const theme = createTheme({
cssVariables: {
colorSchemeSelector: '.demo-disable-transition-%s',
},
colorSchemes: { dark: true },
});
function ModeSwitcher() {
const { mode, setMode } = useColorScheme();
if (!mode) {
return null;
}
return (
<Select
value={mode}
onChange={(event) => setMode(event.target.value)}
sx={{ minWidth: 120 }}
>
<MenuItem value="system">System</MenuItem>
<MenuItem value="light">Light</MenuItem>
<MenuItem value="dark">Dark</MenuItem>
</Select>
);
}
export default function DisableTransitionOnChange() {
const [disableTransition, setDisableTransition] = React.useState(false);
return (
<ThemeProvider
theme={theme}
disableNestedContext
disableTransitionOnChange={disableTransition}
>
<Stack
sx={{
width: '100%',
borderRadius: '4px',
p: 2,
gap: 2,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
bgcolor: 'background.default',
color: 'text.primary',
transition: '1s',
}}
>
<ModeSwitcher />
<FormControlLabel
control={
<Switch
checked={disableTransition}
onChange={(event) => setDisableTransition(event.target.checked)}
/>
}
label="Disable transition"
/>
</Stack>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,71 @@
import * as React from 'react';
import { createTheme, ThemeProvider, useColorScheme } from '@mui/material/styles';
import Stack from '@mui/material/Stack';
import MenuItem from '@mui/material/MenuItem';
import Switch from '@mui/material/Switch';
import Select from '@mui/material/Select';
import FormControlLabel from '@mui/material/FormControlLabel';
const theme = createTheme({
cssVariables: {
colorSchemeSelector: '.demo-disable-transition-%s',
},
colorSchemes: { dark: true },
});
function ModeSwitcher() {
const { mode, setMode } = useColorScheme();
if (!mode) {
return null;
}
return (
<Select
value={mode}
onChange={(event) =>
setMode(event.target.value as 'system' | 'light' | 'dark')
}
sx={{ minWidth: 120 }}
>
<MenuItem value="system">System</MenuItem>
<MenuItem value="light">Light</MenuItem>
<MenuItem value="dark">Dark</MenuItem>
</Select>
);
}
export default function DisableTransitionOnChange() {
const [disableTransition, setDisableTransition] = React.useState(false);
return (
<ThemeProvider
theme={theme}
disableNestedContext
disableTransitionOnChange={disableTransition}
>
<Stack
sx={{
width: '100%',
borderRadius: '4px',
p: 2,
gap: 2,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
bgcolor: 'background.default',
color: 'text.primary',
transition: '1s',
}}
>
<ModeSwitcher />
<FormControlLabel
control={
<Switch
checked={disableTransition}
onChange={(event) => setDisableTransition(event.target.checked)}
/>
}
label="Disable transition"
/>
</Stack>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,64 @@
import * as React from 'react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import FormLabel from '@mui/material/FormLabel';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl';
export default function ModernColorSpaces() {
const colorSpaces = [
{ value: 'color(display-p3 0.7 0.5 0)', label: 'display-p3()' }, // Mud
{ value: 'oklch(0.62 0.25 29)', label: 'oklch()' }, // Orange
{ value: 'oklab(0.59 0.1 -0.14)', label: 'oklab()' }, // Purple
{ value: 'hsl(141, 70%, 48%)', label: 'hsl()' }, // Green
{ value: 'rgb(25, 118, 210)', label: 'rgb()' }, // Blue
];
const [selectedColor, setSelectedColor] = React.useState(colorSpaces[0].value);
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((colorSpace) => (
<FormControlLabel
key={colorSpace.value}
value={colorSpace.value}
control={<Radio />}
label={colorSpace.value}
/>
))}
</RadioGroup>
</FormControl>
<ThemeProvider theme={theme}>
<Button variant="contained" size="large">
Button
</Button>
</ThemeProvider>
</Box>
);
}

View File

@@ -0,0 +1,64 @@
import * as React from 'react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import FormLabel from '@mui/material/FormLabel';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl';
export default function ModernColorSpaces() {
const colorSpaces = [
{ value: 'color(display-p3 0.7 0.5 0)', label: 'display-p3()' }, // Mud
{ value: 'oklch(0.62 0.25 29)', label: 'oklch()' }, // Orange
{ value: 'oklab(0.59 0.1 -0.14)', label: 'oklab()' }, // Purple
{ value: 'hsl(141, 70%, 48%)', label: 'hsl()' }, // Green
{ value: 'rgb(25, 118, 210)', label: 'rgb()' }, // Blue
];
const [selectedColor, setSelectedColor] = React.useState(colorSpaces[0].value);
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((colorSpace) => (
<FormControlLabel
key={colorSpace.value}
value={colorSpace.value}
control={<Radio />}
label={colorSpace.value}
/>
))}
</RadioGroup>
</FormControl>
<ThemeProvider theme={theme}>
<Button variant="contained" size="large">
Button
</Button>
</ThemeProvider>
</Box>
);
}

View File

@@ -0,0 +1,40 @@
import { ThemeProvider, createTheme } from '@mui/material/styles';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions';
import Alert from '@mui/material/Alert';
import Button from '@mui/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>
);
}

View File

@@ -0,0 +1,40 @@
import { ThemeProvider, createTheme } from '@mui/material/styles';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions';
import Alert from '@mui/material/Alert';
import Button from '@mui/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>
);
}

View File

@@ -0,0 +1,128 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { blue, purple, red, green, orange, brown } from '@mui/material/colors';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Typography from '@mui/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 }) {
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"
fontFamily="monospace"
color="text.secondary"
sx={{ wordBreak: 'break-all' }}
>
{color}
</Typography>
</Box>
);
}
ColorDisplay.propTypes = {
color: PropTypes.string.isRequired,
};
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 fontWeight="medium">
theme.alpha(color, 0.5)
</Typography>
<ColorDisplay color={colorValues.alpha} />
</div>
<div>
<Typography variant="subtitle2" gutterBottom fontWeight="medium">
theme.lighten(color, 0.5)
</Typography>
<ColorDisplay color={colorValues.lighten} />
</div>
<div>
<Typography variant="subtitle2" gutterBottom fontWeight="medium">
theme.darken(color, 0.5)
</Typography>
<ColorDisplay color={colorValues.darken} />
</div>
</Box>
</Box>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,123 @@
import * as React from 'react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { blue, purple, red, green, orange, brown } from '@mui/material/colors';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Typography from '@mui/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"
fontFamily="monospace"
color="text.secondary"
sx={{ 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 fontWeight="medium">
theme.alpha(color, 0.5)
</Typography>
<ColorDisplay color={colorValues.alpha} />
</div>
<div>
<Typography variant="subtitle2" gutterBottom fontWeight="medium">
theme.lighten(color, 0.5)
</Typography>
<ColorDisplay color={colorValues.lighten} />
</div>
<div>
<Typography variant="subtitle2" gutterBottom fontWeight="medium">
theme.darken(color, 0.5)
</Typography>
<ColorDisplay color={colorValues.darken} />
</div>
</Box>
</Box>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,321 @@
# CSS theme variables - Configuration
<p class="description">A guide for configuring CSS theme variables in Material UI.</p>
## Customizing variable prefix
To change the default variable prefix (`--mui`), provide a string to `cssVarPrefix` property, as shown below:
```js
createTheme({ cssVariables: { cssVarPrefix: 'any' } });
// generated stylesheet:
// --any-palette-primary-main: ...;
```
To remove the prefix, use an empty string as a value:
```js
createTheme({ cssVariables: { cssVarPrefix: '' } });
// generated stylesheet:
// --palette-primary-main: ...;
```
## Toggling dark mode manually
To toggle between modes manually, set the `colorSchemeSelector` with one of the following selectors:
- `class`: adds a class to the `<html>` element.
```js class
createTheme({
colorSchemes: { light: true, dark: true },
cssVariables: {
colorSchemeSelector: 'class'
}
});
// CSS Result
.light { ... }
.dark { ... }
```
- `data`: adds a data attribute to the `<html>` element.
```js data
createTheme({
colorSchemes: { light: true, dark: true },
cssVariables: {
colorSchemeSelector: 'data'
}
});
// CSS Result
[data-light] { ... }
[data-dark] { ... }
```
- `string`: adds a custom selector to the `<html>` element.
```js string
// The value must start with dot (.) for class or square brackets ([]) for data
createTheme({
colorSchemes: { light: true, dark: true },
cssVariables: {
colorSchemeSelector: '.theme-%s'
}
});
// CSS Result
.theme-light { ... }
.theme-dark { ... }
```
Then, use `useColorScheme` hook to switch between modes:
```jsx
import { useColorScheme } from '@mui/material/styles';
function ModeSwitcher() {
const { mode, setMode } = useColorScheme();
if (!mode) {
return null;
}
return (
<select
value={mode}
onChange={(event) => {
setMode(event.target.value);
// For TypeScript, cast `event.target.value as 'light' | 'dark' | 'system'`:
}}
>
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
);
}
```
:::success
After React hydrates the tree, the mode is set to `system` to follow the user's preference.
:::
### Determining the system mode
To determine if the system mode is `light` or `dark`, use the `systemMode` property:
```js
const { mode, systemMode } = useColorScheme();
console.log(mode); // 'system'
console.log(systemMode); // 'light' | 'dark'
```
However, if the mode is **not** `system`, the `systemMode` will be `undefined`.
```js
const { mode, systemMode } = useColorScheme();
console.log(mode); // 'light' | 'dark'
console.log(systemMode); // undefined
```
### Preventing SSR flickering
For SSR (server-side rendering) applications, Material UI can not detected user-selected mode on the server, causing the screen to flicker from light to dark during the hydration phase on the client.
To prevent the issue, you need to ensure that there is no usage of `theme.palette.mode === 'dark'` in your code base.
If you have such a condition, replace it with the [`theme.applyStyles()` function](/material-ui/customization/dark-mode/#styling-in-dark-mode):
```diff
import Card from '@mui/material/Card';
function App() {
return (
<Card
- sx={(theme) => ({
- backgroundColor: theme.palette.mode === 'dark' ? '#000' : '#fff',
- '&:hover': {
- backgroundColor: theme.palette.mode === 'dark' ? '#333' : '#f5f5f5',
- },
- })}
+ sx={[
+ {
+ backgroundColor: '#fff',
+ '&:hover': {
+ backgroundColor: '#f5f5f5',
+ },
+ },
+ (theme) =>
+ theme.applyStyles('dark', {
+ backgroundColor: '#000',
+ '&:hover': {
+ backgroundColor: '#333',
+ },
+ }),
+ ]}
/>
);
}
```
Next, if you have a custom selector that is **not** `media`, add the [`InitColorSchemeScript`](/material-ui/react-init-color-scheme-script/) component based on the framework that you are using:
:::success
The `attribute` has to be the same as the one you set in the `colorSchemeSelector` property:
```js
createTheme({
cssVariables: {
colorSchemeSelector: 'class'
}
})
<InitColorSchemeScript attribute="class" />
```
:::
### Next.js App Router
Add the following code to the [root layout](https://nextjs.org/docs/app/api-reference/file-conventions/layout#root-layouts) file:
```jsx title="app/layout.js"
import InitColorSchemeScript from '@mui/material/InitColorSchemeScript';
export default function RootLayout(props) {
return (
<html lang="en" suppressHydrationWarning>
<body>
{/* must come before the <main> element */}
<InitColorSchemeScript attribute="class" />
<main>{children}</main>
</body>
</html>
);
}
```
:::warning
If you don't add `suppressHydrationWarning` to your `<html>` tag, you will see warnings about `"Extra attributes from the server"` because `InitColorSchemeScript` updates that element.
:::
### Next.js Pages Router
Add the following code to the custom [`pages/_document.js`](https://nextjs.org/docs/pages/building-your-application/routing/custom-document) file:
```jsx title="pages/_document.js"
import Document, { Html, Head, Main, NextScript } from 'next/document';
import InitColorSchemeScript from '@mui/material/InitColorSchemeScript';
export default class MyDocument extends Document {
render() {
return (
<Html>
<Head>...</Head>
<body>
{/* must come before the <Main> element */}
<InitColorSchemeScript attribute="class" />
<Main />
<NextScript />
</body>
</Html>
);
}
}
```
### Gatsby
Place the script in your [`gatsby-ssr.js`](https://www.gatsbyjs.com/docs/reference/config-files/gatsby-ssr/) file:
```jsx
import * as React from 'react';
import InitColorSchemeScript from '@mui/material/InitColorSchemeScript';
export function onRenderBody({ setPreBodyComponents }) {
setPreBodyComponents([<InitColorSchemeScript attribute="class" />]);
}
```
## Forcing a specific color scheme
To force a specific color scheme for some part of your application, set the selector to the component or HTML element directly.
In the example below, all the components inside the `div` will always be dark:
<codeblock>
```js class
// if the selector is '.mode-%s'
<div className=".mode-dark">
<Paper sx={{ p: 2 }}>
<TextField label="Email" type="email" margin="normal" />
<TextField label="Password" type="password" margin="normal" />
<Button>Sign in</Button>
</Paper>
{/* other components */}
</div>
```
```js data-attribute
// if the selector is '[data-mode-%s]'
<div data-mode-dark>
<Paper sx={{ p: 2 }}>
<TextField label="Email" type="email" margin="normal" />
<TextField label="Password" type="password" margin="normal" />
<Button>Sign in</Button>
</Paper>
{/* other components */}
</div>
```
</codeblock>
## Disabling CSS color scheme
By default, `createTheme()` attaches a [CSS `color-scheme` property](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/color-scheme) based on the palette mode.
You can disable this by setting `disableCssColorScheme` to `true`:
```js
createTheme({
cssVariables: { disableCssColorScheme: true },
});
```
The generated CSS will not include the `color-scheme` property:
```diff
@media (prefers-color-scheme: dark) {
:root {
- color-scheme: dark;
--mui-palette-primary-main: #90caf9;
...
}
}
```
## Instant transition between color schemes
To disable CSS transitions when switching between modes, apply the `disableTransitionOnChange` prop:
```js
<ThemeProvider disableTransitionOnChange />
```
{{"demo": "DisableTransitionOnChange.js"}}
## Force theme recalculation between modes
By default, the `ThemeProvider` does not re-render when switching between light and dark modes when `cssVariables: true` is set in the theme.
If you want to opt-out from this behavior, use the `forceThemeRerender` prop in the ThemeProvider:
```js
<ThemeProvider forceThemeRerender />
```

View File

@@ -0,0 +1,99 @@
# CSS theme variables - Native color
<p class="description">Learn how to use native color with CSS theme variables.</p>
:::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,
},
});
```
{{"demo": "NativeCssColors.js"}}
## 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)',
},
},
});
```
{{"demo": "ModernColorSpaces.js"}}
## 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)',
},
},
});
```
{{"demo": "AliasColorVariables.js"}}
## 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.
{{"demo": "ThemeColorFunctions.js"}}
:::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.
:::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/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.

View File

@@ -0,0 +1,44 @@
# CSS theme variables
<p class="description">An overview of adopting CSS theme variables in Material UI.</p>
[CSS variables](https://www.w3.org/TR/css-variables-1/) are a modern cross-browser feature that let you declare variables in CSS and reuse them in other properties.
You can implement them to improve Material UI's theming and customization experience.
:::info
If this is your first time encountering CSS variables, you should check out [the MDN Web Docs on CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties) before continuing here.
:::
## Introduction
CSS theme variables replace raw values in Material UI components for a better developer experience because, in the browser dev tool, you will see which theme token is used as a value.
In addition 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.
## Advantages
- It lets you prevent [dark-mode SSR flickering](https://github.com/mui/material-ui/issues/27651).
- You can create unlimited color schemes beyond `light` and `dark`.
- 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)](https://web.dev/articles/fcp) | Longer | Since the HTML size is bigger, the time to download the HTML before showing the content is a bit longer. |
| [Time to Interactive (TTI)](https://web.dev/articles/tti) | Shorter (for dark mode) | Stylesheets are not regenerated between light and dark mode, a lot less time is spent running JavaScript code. |
:::warning
The comparison described in the table above may not be applicable to large and complex applications since there are so many factors that can impact performance metrics.
:::
## What's next
- To start a new project with CSS theme variables, check out the [basic usage guide](/material-ui/customization/css-theme-variables/usage/).
- For theming and customization, check out the [how-to guide](/material-ui/customization/css-theme-variables/configuration/).

View File

@@ -0,0 +1,257 @@
# CSS theme variables - Usage
<p class="description">Learn how to adopt CSS theme variables.</p>
## Getting started
To use CSS theme variables, create a theme with `cssVariables: true` and wrap your app with `ThemeProvider`.
After rendering, you'll see CSS variables in the `:root` stylesheet of your HTML document.
By default, these variables are flattened and prefixed with `--mui`:
<codeblock>
```jsx JSX
import { ThemeProvider, createTheme } from '@mui/material/styles';
const theme = createTheme({ cssVariables: true });
function App() {
return <ThemeProvider theme={theme}>{/* ...your app */}</ThemeProvider>;
}
```
```css CSS
:root {
--mui-palette-primary-main: #1976d2;
--mui-palette-primary-light: #42a5f5;
--mui-palette-primary-dark: #1565c0;
--mui-palette-primary-contrastText: #fff;
/* ...other variables */
}
```
</codeblock>
:::info
If you're using the experimental `CssVarsProvider` API, replace it with `ThemeProvider`.
All features that were previously available to the `CssVarsProvider` are now available with the `ThemeProvider`.
:::
## Light and dark modes
When the [built-in dark color scheme](/material-ui/customization/dark-mode/#built-in-support) and `cssVariables` are enabled, both light and dark CSS variables are generated with the default CSS media `prefers-color-scheme` method.
This method works with server-side rendering without extra configuration. However, users won't be able to toggle between modes because the styles are based on the browser media.
If you want to be able to manually toggle modes, see the guide to [toggling dark mode manually](/material-ui/customization/css-theme-variables/configuration/#toggling-dark-mode-manually).
## Applying dark styles
To customize styles for dark mode, use the [`theme.applyStyles()` function](/material-ui/customization/dark-mode/#styling-in-dark-mode).
The example below shows how to customize the Card component for dark mode:
```js
import Card from '@mui/material/Card';
<Card
sx={[
(theme) => ({
backgroundColor: theme.vars.palette.background.default,
}),
(theme) =>
theme.applyStyles('dark', {
backgroundColor: theme.vars.palette.grey[900],
}),
]}
/>;
```
:::warning
Do not use `theme.palette.mode` to switch between light and dark styles—this produces an [unwanted flickering effect](/material-ui/customization/css-theme-variables/configuration/#preventing-ssr-flickering).
:::
## Using theme variables
When the CSS variables feature is enabled, the `vars` node is added to the theme.
This `vars` object mirrors the structure of a serializable theme, with each value corresponding to a CSS variable.
- `theme.vars` (recommended): an object that refers to the CSS theme variables.
```js
const Button = styled('button')(({ theme }) => ({
backgroundColor: theme.vars.palette.primary.main, // var(--mui-palette-primary-main)
color: theme.vars.palette.primary.contrastText, // var(--mui-palette-primary-contrastText)
}));
```
For **TypeScript**, the typings are not enabled by default.
Follow the [TypeScript setup](#typescript) to enable the typings.
:::success
If the components need to render outside of the `CssVarsProvider`, add fallback to the theme object.
```js
backgroundColor: (theme.vars || theme).palette.primary.main;
```
:::
- **Native CSS**: if you can't access the theme object, for example in a pure CSS file, you can use [`var()`](https://developer.mozilla.org/en-US/docs/Web/CSS/var) directly:
```css
/* external-scope.css */
.external-section {
background-color: var(--mui-palette-grey-50);
}
```
## Color channel tokens
Enabling `cssVariables` automatically generates channel tokens which are used to create translucent colors.
These tokens consist of color space channels without the alpha component, separated by spaces.
The colors are suffixed with `Channel`—for example:
```js
const theme = createTheme({ cssVariables: true });
console.log(theme.palette.primary.mainChannel); // '25 118 210'
// This token is generated from `theme.colorSchemes.light.palette.primary.main`.
```
You can use the channel tokens to create a translucent color like this:
```js
const theme = createTheme({
cssVariables: true,
components: {
MuiChip: {
styleOverrides: {
root: ({ theme }) => ({
variants: [
{
props: { variant: 'outlined', color: 'primary' },
style: {
backgroundColor: `rgba(${theme.vars.palette.primary.mainChannel} / 0.12)`,
},
},
],
}),
},
},
},
});
```
:::warning
Don't use a comma (`,`) as a separator because the channel colors use empty spaces to define [transparency](https://www.w3.org/TR/css-color-4/#transparency):
```js
`rgba(${theme.vars.palette.primary.mainChannel}, 0.12)`, // 🚫 this does not work
`rgba(${theme.vars.palette.primary.mainChannel} / 0.12)`, // ✅ always use `/`
```
:::
## Adding new theme tokens
You can add other key-value pairs to the theme input which will be generated as a part of the CSS theme variables:
```js
const theme = createTheme({
cssVariables: true,
colorSchemes: {
light: {
palette: {
// The best part is that you can refer to the variables wherever you like 🤩
gradient:
'linear-gradient(to left, var(--mui-palette-primary-main), var(--mui-palette-primary-dark))',
border: {
subtle: 'var(--mui-palette-neutral-200)',
},
},
},
dark: {
palette: {
gradient:
'linear-gradient(to left, var(--mui-palette-primary-light), var(--mui-palette-primary-main))',
border: {
subtle: 'var(--mui-palette-neutral-600)',
},
},
},
},
});
function App() {
return <ThemeProvider theme={theme}>...</ThemeProvider>;
}
```
Then, you can access those variables from the `theme.vars` object:
```js
const Divider = styled('hr')(({ theme }) => ({
height: 1,
border: '1px solid',
borderColor: theme.vars.palette.border.subtle,
backgroundColor: theme.vars.palette.gradient,
}));
```
Or use `var()` to refer to the CSS variable directly:
```css
/* global.css */
.external-section {
background-color: var(--mui-palette-gradient);
}
```
:::warning
If you're using a [custom prefix](/material-ui/customization/css-theme-variables/configuration/#customizing-variable-prefix), make sure to replace the default `--mui`.
:::
For **TypeScript**, you need to augment the [palette interfaces](#palette-interfaces).
## TypeScript
The theme variables type is not enabled by default. You need to import the module augmentation to enable the typings:
```ts
// The import can be in any file that is included in your `tsconfig.json`
import type {} from '@mui/material/themeCssVarsAugmentation';
import { styled } from '@mui/material/styles';
const StyledComponent = styled('button')(({ theme }) => ({
// ✅ typed-safe
color: theme.vars.palette.primary.main,
}));
```
### Palette interfaces
To add new tokens to the theme palette, you need to augment the `PaletteOptions` and `Palette` interfaces:
```ts
declare module '@mui/material/styles' {
interface PaletteOptions {
gradient: string;
border: {
subtle: string;
};
}
interface Palette {
gradient: string;
border: {
subtle: string;
};
}
}
```
## Next steps
If you need to support system preference and manual selection, check out the [advanced configuration](/material-ui/customization/css-theme-variables/configuration/)

View File

@@ -0,0 +1,114 @@
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Grid';
import Box from '@mui/material/Box';
import { styled, ThemeProvider, useTheme, createTheme } from '@mui/material/styles';
const Root = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
padding: theme.spacing(2),
borderRadius: 4,
[theme.breakpoints.up('md')]: {
padding: theme.spacing(3),
},
}));
const Color = styled(Grid)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
'& div:first-of-type': {
width: theme.spacing(5),
height: theme.spacing(5),
flexShrink: 0,
marginRight: theme.spacing(1.5),
borderRadius: theme.shape.borderRadius,
},
}));
function Demo() {
const theme = useTheme();
const item = (color, name, expanded = false, border = false) => (
<Color size={{ xs: 12, sm: 6, md: expanded ? 8 : 4 }}>
<div
style={{
backgroundColor: color,
border: border ? `1px solid ${theme.palette.divider}` : undefined,
}}
/>
<div>
<Typography variant="body2">{name}</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{color}
</Typography>
</div>
</Color>
);
return (
<Root>
<Typography gutterBottom sx={{ mb: 1.5 }}>
Typography
</Typography>
<Grid container spacing={1}>
{item(theme.palette.text.primary, 'palette.text.primary')}
{item(theme.palette.text.secondary, 'palette.text.secondary')}
{item(theme.palette.text.disabled, 'palette.text.disabled')}
</Grid>
<Typography gutterBottom sx={{ mt: 4, mb: 1.5 }}>
Buttons
</Typography>
<Grid container spacing={1}>
{item(theme.palette.action.active, 'palette.action.active')}
{item(theme.palette.action.hover, 'palette.action.hover')}
{item(theme.palette.action.selected, 'palette.action.selected')}
{item(theme.palette.action.disabled, 'palette.action.disabled')}
{item(
theme.palette.action.disabledBackground,
'palette.action.disabledBackground',
true,
)}
</Grid>
<Typography gutterBottom sx={{ mt: 4, mb: 1.5 }}>
Background
</Typography>
<Grid container spacing={1}>
{item(
theme.palette.background.default,
'palette.background.default',
false,
true,
)}
{item(
theme.palette.background.paper,
'palette.background.paper',
false,
true,
)}
</Grid>
<Typography gutterBottom sx={{ mt: 4, mb: 1.5 }}>
Divider
</Typography>
<Grid container spacing={1}>
{item(theme.palette.divider, 'palette.divider')}
</Grid>
</Root>
);
}
const darkTheme = createTheme({
palette: {
// Switching the dark mode on is a single property value change.
mode: 'dark',
},
});
export default function DarkTheme() {
return (
<Box sx={{ width: '100%' }}>
<ThemeProvider theme={darkTheme}>
<Demo />
</ThemeProvider>
</Box>
);
}

View File

@@ -0,0 +1,62 @@
import Box from '@mui/material/Box';
import { ThemeProvider, useTheme, createTheme } from '@mui/material/styles';
import { amber, deepOrange, grey } from '@mui/material/colors';
const getDesignTokens = (mode) => ({
palette: {
mode,
primary: {
...amber,
...(mode === 'dark' && {
main: amber[300],
}),
},
...(mode === 'dark' && {
background: {
default: deepOrange[900],
paper: deepOrange[900],
},
}),
text: {
...(mode === 'light'
? {
primary: grey[900],
secondary: grey[800],
}
: {
primary: '#fff',
secondary: grey[500],
}),
},
},
});
function MyApp() {
const theme = useTheme();
return (
<Box
sx={{
display: 'flex',
width: '100%',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'background.default',
color: 'text.primary',
borderRadius: 1,
p: 3,
}}
>
This is a {theme.palette.mode} mode theme with custom palette
</Box>
);
}
const darkModeTheme = createTheme(getDesignTokens('dark'));
export default function DarkThemeWithCustomPalette() {
return (
<ThemeProvider theme={darkModeTheme}>
<MyApp />
</ThemeProvider>
);
}

View File

@@ -0,0 +1,67 @@
import Box from '@mui/material/Box';
import {
ThemeProvider,
useTheme,
createTheme,
PaletteMode,
} from '@mui/material/styles';
import { amber, deepOrange, grey } from '@mui/material/colors';
const getDesignTokens = (mode: PaletteMode) => ({
palette: {
mode,
primary: {
...amber,
...(mode === 'dark' && {
main: amber[300],
}),
},
...(mode === 'dark' && {
background: {
default: deepOrange[900],
paper: deepOrange[900],
},
}),
text: {
...(mode === 'light'
? {
primary: grey[900],
secondary: grey[800],
}
: {
primary: '#fff',
secondary: grey[500],
}),
},
},
});
function MyApp() {
const theme = useTheme();
return (
<Box
sx={{
display: 'flex',
width: '100%',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'background.default',
color: 'text.primary',
borderRadius: 1,
p: 3,
}}
>
This is a {theme.palette.mode} mode theme with custom palette
</Box>
);
}
const darkModeTheme = createTheme(getDesignTokens('dark'));
export default function DarkThemeWithCustomPalette() {
return (
<ThemeProvider theme={darkModeTheme}>
<MyApp />
</ThemeProvider>
);
}

View File

@@ -0,0 +1,3 @@
<ThemeProvider theme={darkModeTheme}>
<MyApp />
</ThemeProvider>

View File

@@ -0,0 +1,58 @@
import Box from '@mui/material/Box';
import RadioGroup from '@mui/material/RadioGroup';
import Radio from '@mui/material/Radio';
import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormLabel from '@mui/material/FormLabel';
import { ThemeProvider, createTheme, useColorScheme } from '@mui/material/styles';
function MyApp() {
const { mode, setMode } = useColorScheme();
if (!mode) {
return null;
}
return (
<Box
sx={{
display: 'flex',
width: '100%',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'background.default',
color: 'text.primary',
borderRadius: 1,
p: 3,
minHeight: '56px',
}}
>
<FormControl>
<FormLabel id="demo-theme-toggle">Theme</FormLabel>
<RadioGroup
aria-labelledby="demo-theme-toggle"
name="theme-toggle"
row
value={mode}
onChange={(event) => setMode(event.target.value)}
>
<FormControlLabel value="system" control={<Radio />} label="System" />
<FormControlLabel value="light" control={<Radio />} label="Light" />
<FormControlLabel value="dark" control={<Radio />} label="Dark" />
</RadioGroup>
</FormControl>
</Box>
);
}
const theme = createTheme({
colorSchemes: {
dark: true,
},
});
export default function ToggleColorMode() {
return (
<ThemeProvider theme={theme}>
<MyApp />
</ThemeProvider>
);
}

View File

@@ -0,0 +1,60 @@
import Box from '@mui/material/Box';
import RadioGroup from '@mui/material/RadioGroup';
import Radio from '@mui/material/Radio';
import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormLabel from '@mui/material/FormLabel';
import { ThemeProvider, createTheme, useColorScheme } from '@mui/material/styles';
function MyApp() {
const { mode, setMode } = useColorScheme();
if (!mode) {
return null;
}
return (
<Box
sx={{
display: 'flex',
width: '100%',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'background.default',
color: 'text.primary',
borderRadius: 1,
p: 3,
minHeight: '56px',
}}
>
<FormControl>
<FormLabel id="demo-theme-toggle">Theme</FormLabel>
<RadioGroup
aria-labelledby="demo-theme-toggle"
name="theme-toggle"
row
value={mode}
onChange={(event) =>
setMode(event.target.value as 'system' | 'light' | 'dark')
}
>
<FormControlLabel value="system" control={<Radio />} label="System" />
<FormControlLabel value="light" control={<Radio />} label="Light" />
<FormControlLabel value="dark" control={<Radio />} label="Dark" />
</RadioGroup>
</FormControl>
</Box>
);
}
const theme = createTheme({
colorSchemes: {
dark: true,
},
});
export default function ToggleColorMode() {
return (
<ThemeProvider theme={theme}>
<MyApp />
</ThemeProvider>
);
}

View File

@@ -0,0 +1,3 @@
<ThemeProvider theme={theme}>
<MyApp />
</ThemeProvider>

View File

@@ -0,0 +1,418 @@
# Dark mode
<p class="description">Material UI comes with two palette modes: light (the default) and dark.</p>
## Dark mode only
You can make your application use the dark theme as the default—regardless of the user's preference—by adding `mode: 'dark'` to the `createTheme()` helper:
```js
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
const darkTheme = createTheme({
palette: {
mode: 'dark',
},
});
export default function App() {
return (
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<main>This app is using the dark mode</main>
</ThemeProvider>
);
}
```
Adding `mode: 'dark'` to the `createTheme()` helper modifies several palette values, as shown in the following demo:
{{"demo": "DarkTheme.js", "bg": "inline", "hideToolbar": true}}
Adding `<CssBaseline />` inside of the `<ThemeProvider>` component will also enable dark mode for the app's background.
:::warning
Setting the dark mode this way only works if you are using [the default palette](/material-ui/customization/default-theme/). If you have a custom palette, make sure that you have the correct values based on the `mode`. The next section explains how to do this.
:::
### Overriding the dark palette
To override the default palette, provide a [palette object](/material-ui/customization/palette/#default-colors) with custom colors in hex, RGB, or HSL format:
```jsx
const darkTheme = createTheme({
palette: {
mode: 'dark',
primary: {
main: '#ff5252',
},
},
});
```
Learn more about palette structure in the [Palette documentation](/material-ui/customization/palette/).
## System preference
Some users set a preference for light or dark mode through their operating system—either systemwide, or for individual user agents.
The following sections explain how to apply these preferences to an app's theme.
### Built-in support
Use the `colorSchemes` node to build an application with multiple color schemes.
The built-in color schemes are `light` and `dark` which can be enabled by setting the value to `true`.
The light color scheme is enabled by default, so you only need to set the dark color scheme:
```js
import { ThemeProvider, createTheme } from '@mui/material/styles';
const theme = createTheme({
colorSchemes: {
dark: true,
},
});
function App() {
return <ThemeProvider theme={theme}>...</ThemeProvider>;
}
```
When `colorSchemes` is provided, the following features are activated:
- Automatic switching between light and dark color schemes based on the user's preference
- Synchronization between window tabs—changes to the color scheme in one tab are applied to all other tabs
- An option to [disable transitions](#disable-transitions) when the color scheme changes
:::info
The `colorSchemes` API is an enhanced version of the earlier and more limited `palette` API—the aforementioned features are only accessible with the `colorSchemes` API, so we recommend using it over the `palette` API.
If both `colorSchemes` and `palette` are provided, `palette` will take precedence.
:::
:::success
To test the system preference feature, follow the guide on [emulating the CSS media feature `prefers-color-scheme`](https://developer.chrome.com/docs/devtools/rendering/emulate-css#emulate_css_media_feature_prefers-color-scheme).
:::
### Accessing media prefers-color-scheme
You can make use of this preference with the [`useMediaQuery`](/material-ui/react-use-media-query/) hook and the [`prefers-color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/prefers-color-scheme) media query.
The following demo shows how to check the user's preference in their OS or browser settings:
```jsx
import * as React from 'react';
import useMediaQuery from '@mui/material/useMediaQuery';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
function App() {
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
return <div>prefersDarkMode: {prefersDarkMode.toString()}</div>;
}
```
## Toggling color mode
To give your users a way to toggle between modes for [built-in support](#built-in-support), use the `useColorScheme` hook to read and update the mode.
:::info
The `mode` is always `undefined` on first render, so make sure to handle this case as shown in the demo below—otherwise you may encounter a hydration mismatch error.
:::
{{"demo": "ToggleColorMode.js", "defaultCodeOpen": false}}
## Storage manager
By default, the [built-in support](#built-in-support) for color schemes uses the browser's `localStorage` API to store the user's mode and scheme preference.
To use a different storage manager, create a custom function with this signature:
```ts
type Unsubscribe = () => void;
function storageManager(params: { key: string }): {
get: (defaultValue: any) => any;
set: (value: any) => void;
subscribe: (handler: (value: any) => void) => Unsubscribe;
};
```
Then pass it to the `storageManager` prop of the `ThemeProvider` component:
```tsx
import { ThemeProvider, createTheme } from '@mui/material/styles';
import type { StorageManager } from '@mui/material/styles';
const theme = createTheme({
colorSchemes: {
dark: true,
},
});
function storageManager(params): StorageManager {
return {
get: (defaultValue) => {
// Your implementation
},
set: (value) => {
// Your implementation
},
subscribe: (handler) => {
// Your implementation
return () => {
// cleanup
};
},
};
}
function App() {
return (
<ThemeProvider theme={theme} storageManager={storageManager}>
...
</ThemeProvider>
);
}
```
:::warning
If you are using the `InitColorSchemeScript` component to [prevent SSR flickering](/material-ui/customization/css-theme-variables/configuration/#preventing-ssr-flickering), you have to include the `localStorage` implementation in your custom storage manager.
:::
### Disable storage
To disable the storage manager, pass `null` to the `storageManager` prop:
```tsx
<ThemeProvider theme={theme} storageManager={null}>
...
</ThemeProvider>
```
:::warning
Disabling the storage manager will cause the app to reset to its default mode whenever the user refreshes the page.
:::
## Disable transitions
To instantly switch between color schemes with no transition, apply the `disableTransitionOnChange` prop to the `ThemeProvider` component:
```jsx
<ThemeProvider theme={theme} disableTransitionOnChange>
...
</ThemeProvider>
```
## Disable double rendering
By default, the `ThemeProvider` rerenders when the theme contains light **and** dark color schemes to prevent SSR hydration mismatches.
To disable this behavior, use the `noSsr` prop:
```jsx
<ThemeProvider theme={theme} noSsr>
```
`noSsr` is useful if you are building:
- A client-only application, such as a single-page application (SPA). This prop will optimize the performance and prevent the dark mode flickering when users refresh the page.
- A server-rendered application with [Suspense](https://react.dev/reference/react/Suspense). However, you must ensure that the server render output matches the initial render output on the client.
## Setting the default mode
When `colorSchemes` is provided, the default mode is `system`, which means the app uses the system preference when users first visit the site.
To set a different default mode, pass the `defaultMode` prop to the ThemeProvider component:
```js
<ThemeProvider theme={theme} defaultMode="dark">
```
:::info
The `defaultMode` value can be `'light'`, `'dark'`, or `'system'`.
:::
### InitColorSchemeScript component
If you are using the `InitColorSchemeScript` component to [prevent SSR flicker](/material-ui/customization/css-theme-variables/configuration/#preventing-ssr-flickering), you have to set the `defaultMode` with the same value you passed to the `ThemeProvider` component:
```js
<InitColorSchemeScript defaultMode="dark">
```
## Styling in dark mode
Use the `theme.applyStyles()` utility to apply styles for a specific mode.
We recommend using this function over checking `theme.palette.mode` to switch between styles as it has more benefits:
<!-- #target-branch-reference -->
- It can be used with [Pigment CSS](https://github.com/mui/material-ui/tree/master/packages/pigment-css-react), our in-house zero-runtime CSS-in-JS solution.
- It is generally more readable and maintainable.
- It is slightly more performant as it doesn't require to do style recalculation but the bundle size of SSR generated styles is larger.
### Usage
With the `styled` function:
```jsx
import { styled } from '@mui/material/styles';
const MyComponent = styled('div')(({ theme }) => [
{
color: '#fff',
backgroundColor: theme.palette.primary.main,
'&:hover': {
boxShadow: theme.shadows[3],
backgroundColor: theme.palette.primary.dark,
},
},
theme.applyStyles('dark', {
backgroundColor: theme.palette.secondary.main,
'&:hover': {
backgroundColor: theme.palette.secondary.dark,
},
}),
]);
```
With the `sx` prop:
```jsx
import Button from '@mui/material/Button';
<Button
sx={[
(theme) => ({
color: '#fff',
backgroundColor: theme.palette.primary.main,
'&:hover': {
boxShadow: theme.shadows[3],
backgroundColor: theme.palette.primary.dark,
},
}),
(theme) =>
theme.applyStyles('dark', {
backgroundColor: theme.palette.secondary.main,
'&:hover': {
backgroundColor: theme.palette.secondary.dark,
},
}),
]}
>
Submit
</Button>;
```
:::warning
When `cssVariables: true`, styles applied with `theme.applyStyles()` have higher specificity than those defined outside of it.
So if you need to override styles, you must also use `theme.applyStyles()` as shown below:
```jsx
const BaseButton = styled('button')(({ theme }) =>
theme.applyStyles('dark', {
backgroundColor: 'white',
}),
);
const AliceblueButton = styled(BaseButton)({
backgroundColor: 'aliceblue', // In dark mode, backgroundColor will be white as theme.applyStyles() has higher specificity
});
const PinkButton = styled(BaseButton)(({ theme }) =>
theme.applyStyles('dark', {
backgroundColor: 'pink', // In dark mode, backgroundColor will be pink
}),
);
```
:::
### API
`theme.applyStyles(mode, styles) => CSSObject`
Apply styles for a specific mode.
#### Arguments
- `mode` (`'light' | 'dark'`) - The mode for which the styles should be applied.
- `styles` (`CSSObject`) - An object that contains the styles to be applied for the specified mode.
#### Overriding applyStyles
You can override `theme.applyStyles()` with a custom function to gain complete control over the values it returns.
Please review the [source code](https://github.com/mui/material-ui/blob/HEAD/packages/mui-system/src/createTheme/applyStyles.ts) to understand how the default implementation works before overriding it.
For instance, if you need the function to return a string instead of an object so it can be used inside template literals:
```js
const theme = createTheme({
cssVariables: {
colorSchemeSelector: '.mode-%s',
},
colorSchemes: {
dark: {},
light: {},
},
applyStyles: function (key: string, styles: any) {
// return a string instead of an object
return `*:where(.mode-${key}) & {${styles}}`;
},
});
const StyledButton = styled('button')`
${theme.applyStyles(
'dark', `
background: white;
`
)}
`;
```
### Codemod
We provide codemods to migrate your codebase from using `theme.palette.mode` to use `theme.applyStyles()`.
You can run each codemod below or all of them at once.
```bash
npx @mui/codemod@latest v6.0.0/styled <path/to/folder-or-file>
npx @mui/codemod@latest v6.0.0/sx-prop <path/to/folder-or-file>
npx @mui/codemod@latest v6.0.0/theme-v6 <path/to/theme-file>
```
> Run `v6.0.0/theme-v6` against the file that contains the custom `styleOverrides`. Ignore this codemod if you don't have a custom theme.
## Dark mode flicker
### The problem
Server-rendered apps are built before they reach the user's device.
This means they can't automatically adjust to the user's preferred color scheme when first loaded.
Here's what typically happens:
1. You load the app and set it to dark mode.
2. You refresh the page.
3. The app briefly appears in light mode (the default).
4. Then it switches back to dark mode once the app fully loads.
This "flash" of light mode happens every time you open the app, as long as your browser remembers your dark mode preference.
This sudden change can be jarring, especially in low-light environments.
It can strain your eyes and disrupt your experience, particularly if you interact with the app during this transition.
To better understand this issue, take a look at the animated image below:
<img src="/static/joy-ui/dark-mode/dark-mode-flicker.gif" style="width: 814px; border-radius: 8px;" alt="An example video that shows a page that initially loads correctly in dark mode but quickly flickers to light mode." width="1628" height="400" />
### The solution: CSS variables
Solving this problem requires a novel approach to styling and theming.
(See this [RFC on CSS variables support](https://github.com/mui/material-ui/issues/27651) to learn more about the implementation of this feature.)
For applications that need to support light and dark mode using CSS media `prefers-color-scheme`, enabling the [CSS variables feature](/material-ui/customization/css-theme-variables/usage/#light-and-dark-modes) fixes the issue.
But if you want to be able to toggle between modes manually, avoiding the flicker requires a combination of CSS variables and the `InitColorSchemeScript` component.
Check out the [Preventing SSR flicker](/material-ui/customization/css-theme-variables/configuration/#preventing-ssr-flickering) section for more details.

View File

@@ -0,0 +1,198 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Divider from '@mui/material/Divider';
import { createTheme, styled } from '@mui/material/styles';
import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';
import { useTranslate } from '@mui/docs/i18n';
import ThemeViewer, {
useItemIdsLazy,
} from 'docs/src/modules/components/ThemeViewer';
import { blue, grey } from '@mui/docs/branding';
const StyledSwitch = styled(Switch)(({ theme }) => [
{
display: 'flex',
padding: 0,
width: 32,
height: 20,
borderRadius: 99,
'&:active': {
'& .MuiSwitch-thumb': {
width: 16,
},
'& .MuiSwitch-switchBase.Mui-checked': {
transform: 'translateX(9px)',
},
},
'& .MuiSwitch-switchBase': {
padding: 2,
'&.Mui-checked': {
transform: 'translateX(12px)',
color: '#FFF',
'& + .MuiSwitch-track': {
opacity: 1,
backgroundColor: blue[500],
},
},
},
'& .MuiSwitch-thumb': {
width: 16,
height: 16,
borderRadius: 99,
transition: theme.transitions.create(['width'], {
duration: 200,
}),
},
'& .MuiSwitch-track': {
borderRadius: 16 / 2,
opacity: 1,
backgroundColor: grey[400],
boxSizing: 'border-box',
},
[`:where(${theme.vars ? '[data-mui-color-scheme="dark"]' : '.mode-dark'}) &`]: {
'& .MuiSwitch-switchBase': {
'&.Mui-checked': {
'& + .MuiSwitch-track': {
backgroundColor: blue[500],
},
},
},
'& .MuiSwitch-track': {
backgroundColor: grey[700],
},
},
},
]);
function DefaultTheme() {
const [checked, setChecked] = React.useState(false);
const [expandPaths, setExpandPaths] = React.useState(null);
const t = useTranslate();
const [darkTheme, setDarkTheme] = React.useState(false);
React.useEffect(() => {
let expandPath;
decodeURI(document.location.search.slice(1))
.split('&')
.forEach((param) => {
const [name, value] = param.split('=');
if (name === 'expand-path') {
expandPath = value;
}
});
if (!expandPath) {
return;
}
setExpandPaths(
expandPath
.replace('$.', '')
.split('.')
.reduce((acc, path) => {
const last = acc.length > 0 ? `${acc[acc.length - 1]}.` : '';
acc.push(last + path);
return acc;
}, []),
);
}, []);
const data = React.useMemo(() => {
const themeData = createTheme({
palette: { mode: darkTheme ? 'dark' : 'light' },
});
const {
unstable_sxConfig: unstableSxConfig,
unstable_sx: unstableSx,
...rest
} = themeData;
return rest;
}, [darkTheme]);
const allNodeIds = useItemIdsLazy(data);
React.useDebugValue(allNodeIds);
const currentExpandPaths = React.useMemo(() => {
if (expandPaths !== null) {
return expandPaths;
}
return checked ? allNodeIds : [];
}, [checked, allNodeIds, expandPaths]);
const collapsedThemeViewer = React.useMemo(
() => <ThemeViewer data={data} expandPaths={[]} />,
[data],
);
const expandedThemeViewer = React.useMemo(
() => <ThemeViewer data={data} expandPaths={allNodeIds} />,
[data, allNodeIds],
);
return (
<Box sx={{ width: '100%' }}>
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
<FormControlLabel
label={t('expandAll')}
sx={{
m: 0,
flexDirection: 'row-reverse',
gap: 1,
'& .MuiFormControlLabel-label': {
fontFamily: 'IBM Plex Sans',
color: 'text.secondary',
},
}}
control={
<StyledSwitch
size="small"
checked={checked}
onChange={(event) => {
setChecked(event.target.checked);
}}
/>
}
/>
<Divider orientation="vertical" flexItem />
<FormControlLabel
label={t('useDarkTheme')}
sx={{
m: 0,
flexDirection: 'row-reverse',
gap: 1,
'& .MuiFormControlLabel-label': {
fontFamily: 'IBM Plex Sans',
color: 'text.secondary',
},
}}
control={
<StyledSwitch
size="small"
checked={darkTheme}
onChange={(event) => {
setDarkTheme(event.target.checked);
}}
/>
}
/>
</Box>
{expandPaths !== null ? (
<ThemeViewer data={data} expandPaths={currentExpandPaths} />
) : (
<React.Fragment>
<Box sx={{ display: checked ? 'none' : 'block' }}>
{collapsedThemeViewer}
</Box>
<Box sx={{ display: checked ? 'block' : 'none' }}>
{expandedThemeViewer}
</Box>
</React.Fragment>
)}
</Box>
);
}
export default DefaultTheme;

View File

@@ -0,0 +1,17 @@
# Default theme viewer
<p class="description">This tree view allows you to explore how the theme object looks like with the default values.</p>
If you want to learn more about how the theme is assembled, take a look at [`material-ui/style/createTheme.ts`](https://github.com/mui/material-ui/blob/-/packages/mui-material/src/styles/createTheme.ts),
and the related imports which `createTheme()` uses.
You can play with the documentation theme object in your browser console,
as the `theme` variable is exposed on all the documentation pages.
:::warning
Please note that **the documentation site is using a custom theme** (the MUI's organization branding).
:::
<hr/>
{{"demo": "DefaultTheme.js", "hideToolbar": true, "bg": "inline"}}

View File

@@ -0,0 +1,121 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Input from '@mui/material/Input';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';
import IncreaseIcon from '@mui/icons-material/AddCircleOutline';
import DecreaseIcon from '@mui/icons-material/RemoveCircleOutline';
import {
DispatchContext,
ThemeOptionsContext,
} from 'docs/src/modules/components/ThemeContext';
import { useTranslate } from '@mui/docs/i18n';
import { setDocsSpacing, resetDocsSpacing } from 'docs/src/BrandingCssVarsProvider';
const minSpacing = 0;
const maxSpacing = 20;
export default function DensityTool() {
const [spacingUnit, setSpacingUnit] = React.useState(8);
const dispatch = React.useContext(DispatchContext);
const themeOptions = React.useContext(ThemeOptionsContext);
const handleDensityChange = (event) => {
dispatch({
type: 'SET_DENSE',
payload: event.target.checked,
});
};
const handleSpacingChange = (event, value) => {
let spacing = value || +event.target.value;
// If the entered value is greater than maxSpacing, setting up maxSpacing as value
if (spacing > maxSpacing) {
spacing = maxSpacing;
}
// If the entered value is less than minSpacing, setting up minSpacing as value
if (spacing < minSpacing) {
spacing = minSpacing;
}
setSpacingUnit(spacing);
setDocsSpacing(spacing);
};
const resetDensity = () => {
setSpacingUnit(8);
resetDocsSpacing();
dispatch({ type: 'RESET_DENSITY' });
};
const t = useTranslate();
return (
<Box
sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: 2,
}}
>
<FormControlLabel
control={
<Switch
checked={themeOptions.dense}
onChange={handleDensityChange}
value="dense"
color="secondary"
/>
}
label={t('useHighDensity')}
/>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography id="input-slider" gutterBottom>
{t('spacingUnit')}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<IconButton
aria-label={t('decreaseSpacing')}
onClick={() => {
setSpacingUnit(spacingUnit - 1);
setDocsSpacing(spacingUnit - 1);
}}
disabled={spacingUnit === minSpacing}
>
<DecreaseIcon />
</IconButton>
<Input
value={spacingUnit}
margin="dense"
onChange={handleSpacingChange}
inputProps={{
step: 1,
min: minSpacing,
max: maxSpacing,
type: 'number',
'aria-labelledby': 'input-slider',
}}
/>
<IconButton
aria-label={t('increaseSpacing')}
onClick={() => {
setSpacingUnit(spacingUnit + 1);
setDocsSpacing(spacingUnit + 1);
}}
disabled={spacingUnit === maxSpacing}
>
<IncreaseIcon />
</IconButton>
</Box>
</Box>
<Button variant="contained" onClick={resetDensity}>
{t('resetDensity')}
</Button>
</Box>
);
}

View File

@@ -0,0 +1,119 @@
# Density
<p class="description">How to apply density to Material UI components.</p>
## Applying density
This section explains how to apply density.
It doesn't cover potential use cases, or considerations for using density in your application.
The Material Design guidelines have a [comprehensive guide](https://m2.material.io/design/layout/applying-density.html) covering these topics in more detail.
## Implementing density
Higher density can be applied to some components via props. The component pages
have at least one example using the respective component with higher density applied.
Depending on the component, density is applied either via lower spacing, or simply by
reducing the size.
The following components have props applying higher density:
- [Button](/material-ui/api/button/)
- [Fab](/material-ui/api/fab/)
- [FilledInput](/material-ui/api/filled-input/)
- [FormControl](/material-ui/api/form-control/)
- [FormHelperText](/material-ui/api/form-helper-text/)
- [IconButton](/material-ui/api/icon-button/)
- [InputBase](/material-ui/api/input-base/)
- [InputLabel](/material-ui/api/input-label/)
- [ListItem](/material-ui/api/list-item/)
- [OutlinedInput](/material-ui/api/outlined-input/)
- [Table](/material-ui/api/table/)
- [TextField](/material-ui/api/text-field/)
- [Toolbar](/material-ui/api/toolbar/)
## Explore theme density
This tool allows you to apply density via spacing and component props. You can browse
around and see how this applies to the overall feel of Material UI components.
If you enable high density a custom theme is applied to the docs. This theme is only
for demonstration purposes. You _should not_ apply this theme to your whole application
as this might negatively impact user experience. The [Material Design guidelines](https://m2.material.io/design/layout/applying-density.html) has examples
for when not to apply density.
The theme is configured with the following options:
```js
const theme = createTheme({
components: {
MuiButton: {
defaultProps: {
size: 'small',
},
},
MuiFilledInput: {
defaultProps: {
margin: 'dense',
},
},
MuiFormControl: {
defaultProps: {
margin: 'dense',
},
},
MuiFormHelperText: {
defaultProps: {
margin: 'dense',
},
},
MuiIconButton: {
defaultProps: {
size: 'small',
},
},
MuiInputBase: {
defaultProps: {
margin: 'dense',
},
},
MuiInputLabel: {
defaultProps: {
margin: 'dense',
},
},
MuiListItem: {
defaultProps: {
dense: true,
},
},
MuiOutlinedInput: {
defaultProps: {
margin: 'dense',
},
},
MuiFab: {
defaultProps: {
size: 'small',
},
},
MuiTable: {
defaultProps: {
size: 'small',
},
},
MuiTextField: {
defaultProps: {
margin: 'dense',
},
},
MuiToolbar: {
defaultProps: {
variant: 'dense',
},
},
},
});
```
{{"demo": "DensityTool.js", "hideToolbar": true}}

View File

@@ -0,0 +1,16 @@
import Slider from '@mui/material/Slider';
export default function DevTools() {
return (
<Slider
defaultValue={30}
sx={{
width: 300,
color: 'success.main',
'& .MuiSlider-thumb': {
borderRadius: '1px',
},
}}
/>
);
}

View File

@@ -0,0 +1,16 @@
import Slider from '@mui/material/Slider';
export default function DevTools() {
return (
<Slider
defaultValue={30}
sx={{
width: 300,
color: 'success.main',
'& .MuiSlider-thumb': {
borderRadius: '1px',
},
}}
/>
);
}

View File

@@ -0,0 +1,10 @@
<Slider
defaultValue={30}
sx={{
width: 300,
color: 'success.main',
'& .MuiSlider-thumb': {
borderRadius: '1px',
},
}}
/>

View File

@@ -0,0 +1,52 @@
import * as React from 'react';
import { alpha, styled } from '@mui/material/styles';
import Slider from '@mui/material/Slider';
import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';
const StyledSlider = styled(Slider, {
shouldForwardProp: (prop) => prop !== 'success',
})(({ theme }) => ({
width: 300,
variants: [
{
props: ({ success }) => success,
style: {
color: theme.palette.success.main,
'& .MuiSlider-thumb': {
[`&:hover, &.Mui-focusVisible`]: {
boxShadow: `0px 0px 0px 8px ${alpha(theme.palette.success.main, 0.16)}`,
},
[`&.Mui-active`]: {
boxShadow: `0px 0px 0px 14px ${alpha(theme.palette.success.main, 0.16)}`,
},
},
},
},
],
}));
export default function DynamicCSS() {
const [success, setSuccess] = React.useState(false);
const handleChange = (event) => {
setSuccess(event.target.checked);
};
return (
<React.Fragment>
<FormControlLabel
control={
<Switch
checked={success}
onChange={handleChange}
color="primary"
value="dynamic-class-name"
/>
}
label="Success"
/>
<StyledSlider success={success} defaultValue={30} sx={{ mt: 1 }} />
</React.Fragment>
);
}

View File

@@ -0,0 +1,56 @@
import * as React from 'react';
import { alpha, styled } from '@mui/material/styles';
import Slider, { SliderProps } from '@mui/material/Slider';
import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';
interface StyledSliderProps extends SliderProps {
success?: boolean;
}
const StyledSlider = styled(Slider, {
shouldForwardProp: (prop) => prop !== 'success',
})<StyledSliderProps>(({ theme }) => ({
width: 300,
variants: [
{
props: ({ success }) => success,
style: {
color: theme.palette.success.main,
'& .MuiSlider-thumb': {
[`&:hover, &.Mui-focusVisible`]: {
boxShadow: `0px 0px 0px 8px ${alpha(theme.palette.success.main, 0.16)}`,
},
[`&.Mui-active`]: {
boxShadow: `0px 0px 0px 14px ${alpha(theme.palette.success.main, 0.16)}`,
},
},
},
},
],
}));
export default function DynamicCSS() {
const [success, setSuccess] = React.useState(false);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSuccess(event.target.checked);
};
return (
<React.Fragment>
<FormControlLabel
control={
<Switch
checked={success}
onChange={handleChange}
color="primary"
value="dynamic-class-name"
/>
}
label="Success"
/>
<StyledSlider success={success} defaultValue={30} sx={{ mt: 1 }} />
</React.Fragment>
);
}

View File

@@ -0,0 +1,14 @@
<React.Fragment>
<FormControlLabel
control={
<Switch
checked={success}
onChange={handleChange}
color="primary"
value="dynamic-class-name"
/>
}
label="Success"
/>
<StyledSlider success={success} defaultValue={30} sx={{ mt: 1 }} />
</React.Fragment>

View File

@@ -0,0 +1,53 @@
import * as React from 'react';
import { styled } from '@mui/material/styles';
import Slider from '@mui/material/Slider';
import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';
const CustomSlider = styled(Slider)({
width: 300,
color: 'var(--color)',
'& .MuiSlider-thumb': {
[`&:hover, &.Mui-focusVisible`]: {
boxShadow: '0px 0px 0px 8px var(--box-shadow)',
},
[`&.Mui-active`]: {
boxShadow: '0px 0px 0px 14px var(--box-shadow)',
},
},
});
const successVars = {
'--color': '#4caf50',
'--box-shadow': 'rgb(76, 175, 80, .16)',
};
const defaultVars = {
'--color': '#1976d2',
'--box-shadow': 'rgb(25, 118, 210, .16)',
};
export default function DynamicCSSVariables() {
const [vars, setVars] = React.useState(defaultVars);
const handleChange = (event) => {
setVars(event.target.checked ? successVars : defaultVars);
};
return (
<React.Fragment>
<FormControlLabel
control={
<Switch
checked={vars === successVars}
onChange={handleChange}
color="primary"
value="dynamic-class-name"
/>
}
label="Success"
/>
<CustomSlider style={vars} defaultValue={30} sx={{ mt: 1 }} />
</React.Fragment>
);
}

View File

@@ -0,0 +1,53 @@
import * as React from 'react';
import { styled } from '@mui/material/styles';
import Slider from '@mui/material/Slider';
import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';
const CustomSlider = styled(Slider)({
width: 300,
color: 'var(--color)',
'& .MuiSlider-thumb': {
[`&:hover, &.Mui-focusVisible`]: {
boxShadow: '0px 0px 0px 8px var(--box-shadow)',
},
[`&.Mui-active`]: {
boxShadow: '0px 0px 0px 14px var(--box-shadow)',
},
},
});
const successVars = {
'--color': '#4caf50',
'--box-shadow': 'rgb(76, 175, 80, .16)',
} as React.CSSProperties;
const defaultVars = {
'--color': '#1976d2',
'--box-shadow': 'rgb(25, 118, 210, .16)',
} as React.CSSProperties;
export default function DynamicCSSVariables() {
const [vars, setVars] = React.useState<React.CSSProperties>(defaultVars);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setVars(event.target.checked ? successVars : defaultVars);
};
return (
<React.Fragment>
<FormControlLabel
control={
<Switch
checked={vars === successVars}
onChange={handleChange}
color="primary"
value="dynamic-class-name"
/>
}
label="Success"
/>
<CustomSlider style={vars} defaultValue={30} sx={{ mt: 1 }} />
</React.Fragment>
);
}

View File

@@ -0,0 +1,14 @@
<React.Fragment>
<FormControlLabel
control={
<Switch
checked={vars === successVars}
onChange={handleChange}
color="primary"
value="dynamic-class-name"
/>
}
label="Success"
/>
<CustomSlider style={vars} defaultValue={30} sx={{ mt: 1 }} />
</React.Fragment>

View File

@@ -0,0 +1,11 @@
import * as React from 'react';
import GlobalStyles from '@mui/material/GlobalStyles';
export default function GlobalCssOverride() {
return (
<React.Fragment>
<GlobalStyles styles={{ h1: { color: 'grey' } }} />
<h1>Grey h1 element</h1>
</React.Fragment>
);
}

View File

@@ -0,0 +1,11 @@
import * as React from 'react';
import GlobalStyles from '@mui/material/GlobalStyles';
export default function GlobalCssOverride() {
return (
<React.Fragment>
<GlobalStyles styles={{ h1: { color: 'grey' } }} />
<h1>Grey h1 element</h1>
</React.Fragment>
);
}

View File

@@ -0,0 +1,4 @@
<React.Fragment>
<GlobalStyles styles={{ h1: { color: 'grey' } }} />
<h1>Grey h1 element</h1>
</React.Fragment>

View File

@@ -0,0 +1,15 @@
import * as React from 'react';
import GlobalStyles from '@mui/material/GlobalStyles';
export default function GlobalCssOverrideTheme() {
return (
<React.Fragment>
<GlobalStyles
styles={(theme) => ({
h1: { color: theme.palette.primary.main },
})}
/>
<h1>Grey h1 element</h1>
</React.Fragment>
);
}

View File

@@ -0,0 +1,15 @@
import * as React from 'react';
import GlobalStyles from '@mui/material/GlobalStyles';
export default function GlobalCssOverrideTheme() {
return (
<React.Fragment>
<GlobalStyles
styles={(theme) => ({
h1: { color: theme.palette.primary.main },
})}
/>
<h1>Grey h1 element</h1>
</React.Fragment>
);
}

View File

@@ -0,0 +1,8 @@
<React.Fragment>
<GlobalStyles
styles={(theme) => ({
h1: { color: theme.palette.primary.main },
})}
/>
<h1>Grey h1 element</h1>
</React.Fragment>

View File

@@ -0,0 +1,28 @@
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider, createTheme } from '@mui/material/styles';
const theme = createTheme({
palette: {
success: {
main: '#ff0000',
},
},
components: {
MuiCssBaseline: {
styleOverrides: (themeParam) => `
h1 {
color: ${themeParam.palette.success.main};
}
`,
},
},
});
export default function OverrideCallbackCssBaseline() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<h1>h1 element</h1>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,28 @@
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider, createTheme } from '@mui/material/styles';
const theme = createTheme({
palette: {
success: {
main: '#ff0000',
},
},
components: {
MuiCssBaseline: {
styleOverrides: (themeParam) => `
h1 {
color: ${themeParam.palette.success.main};
}
`,
},
},
});
export default function OverrideCallbackCssBaseline() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<h1>h1 element</h1>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,4 @@
<ThemeProvider theme={theme}>
<CssBaseline />
<h1>h1 element</h1>
</ThemeProvider>

View File

@@ -0,0 +1,23 @@
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider, createTheme } from '@mui/material/styles';
const theme = createTheme({
components: {
MuiCssBaseline: {
styleOverrides: `
h1 {
color: grey;
}
`,
},
},
});
export default function OverrideCssBaseline() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<h1>Grey h1 element</h1>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,23 @@
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider, createTheme } from '@mui/material/styles';
const theme = createTheme({
components: {
MuiCssBaseline: {
styleOverrides: `
h1 {
color: grey;
}
`,
},
},
});
export default function OverrideCssBaseline() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<h1>Grey h1 element</h1>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,4 @@
<ThemeProvider theme={theme}>
<CssBaseline />
<h1>Grey h1 element</h1>
</ThemeProvider>

View File

@@ -0,0 +1,19 @@
import Slider from '@mui/material/Slider';
import { alpha, styled } from '@mui/material/styles';
const SuccessSlider = styled(Slider)(({ theme }) => ({
width: 300,
color: theme.palette.success.main,
'& .MuiSlider-thumb': {
'&:hover, &.Mui-focusVisible': {
boxShadow: `0px 0px 0px 8px ${alpha(theme.palette.success.main, 0.16)}`,
},
'&.Mui-active': {
boxShadow: `0px 0px 0px 14px ${alpha(theme.palette.success.main, 0.16)}`,
},
},
}));
export default function StyledCustomization() {
return <SuccessSlider defaultValue={30} />;
}

View File

@@ -0,0 +1,19 @@
import Slider, { SliderProps } from '@mui/material/Slider';
import { alpha, styled } from '@mui/material/styles';
const SuccessSlider = styled(Slider)<SliderProps>(({ theme }) => ({
width: 300,
color: theme.palette.success.main,
'& .MuiSlider-thumb': {
'&:hover, &.Mui-focusVisible': {
boxShadow: `0px 0px 0px 8px ${alpha(theme.palette.success.main, 0.16)}`,
},
'&.Mui-active': {
boxShadow: `0px 0px 0px 14px ${alpha(theme.palette.success.main, 0.16)}`,
},
},
}));
export default function StyledCustomization() {
return <SuccessSlider defaultValue={30} />;
}

View File

@@ -0,0 +1 @@
<SuccessSlider defaultValue={30} />

View File

@@ -0,0 +1,5 @@
import Slider from '@mui/material/Slider';
export default function SxProp() {
return <Slider defaultValue={30} sx={{ width: 300, color: 'success.main' }} />;
}

View File

@@ -0,0 +1,5 @@
import Slider from '@mui/material/Slider';
export default function SxProp() {
return <Slider defaultValue={30} sx={{ width: 300, color: 'success.main' }} />;
}

View File

@@ -0,0 +1 @@
<Slider defaultValue={30} sx={{ width: 300, color: 'success.main' }} />

View File

@@ -0,0 +1,221 @@
---
productId: material-ui
components: GlobalStyles
---
# How to customize
<p class="description">Learn how to customize Material UI components by taking advantage of different strategies for specific use cases.</p>
Material UI provides several different ways to customize a component's styles. Your specific context will determine which one is ideal. From narrowest to broadest use case, here are the options:
1. [One-off customization](#1-one-off-customization)
1. [Reusable component](#2-reusable-component)
1. [Global theme overrides](#3-global-theme-overrides)
1. [Global CSS override](#4-global-css-override)
## 1. One-off customization
To change the styles of _one single instance_ of a component, you can use one of the following options:
### The `sx` prop
The [`sx` prop](/system/getting-started/the-sx-prop/) is the best option for adding style overrides to a single instance of a component in most cases.
It can be used with all Material UI components.
{{"demo": "SxProp.js"}}
### Overriding nested component styles
To customize a specific part of a component, you can use the class name provided by Material UI inside the `sx` prop. As an example, let's say you want to change the `Slider` component's thumb from a circle to a square.
First, use your browser's dev tools to identify the class for the component slot you want to override.
The styles injected into the DOM by Material UI rely on class names that all [follow a standard pattern](https://v6.mui.com/system/styles/advanced/#class-names):
`[hash]-Mui[Component name]-[name of the slot]`.
In this case, the styles are applied with `.css-ae2u5c-MuiSlider-thumb` but you only really need to target the `.MuiSlider-thumb`, where `Slider` is the component and `thumb` is the slot. Use this class name to write a CSS selector within the `sx` prop (`& .MuiSlider-thumb`), and add your overrides.
<img src="/static/images/customization/dev-tools.png" alt="dev-tools" style="margin-bottom: 16px;" width="2400" height="800" />
{{"demo": "DevTools.js"}}
:::warning
These class names can't be used as CSS selectors because they are unstable.
:::
### Overriding styles with class names
If you want to override a component's styles using custom classes, you can use the `className` prop, available on each component.
To override the styles of a specific part of the component, use the global classes provided by Material UI, as described in the previous section **"Overriding nested component styles"** under the [`sx` prop section](#the-sx-prop).
Visit the [Style library interoperability](/material-ui/integrations/interoperability/) guide to find examples of this approach using different styling libraries.
### State classes
States like _hover_, _focus_, _disabled_ and _selected_, are styled with a higher CSS specificity. To customize them, you'll need to **increase specificity**.
Here is an example with the _disabled_ state and the `Button` component using a pseudo-class (`:disabled`):
```css
.Button {
color: black;
}
/* Increase the specificity */
.Button:disabled {
color: white;
}
```
```jsx
<Button disabled className="Button">
```
You can't always use a CSS pseudo-class, as the state doesn't exist in the web specification.
Let's take the `MenuItem` component and its _selected_ state as an example.
In this situation, you can use Material UI's **state classes**, which act just like CSS pseudo-classes.
Target the `.Mui-selected` global class name to customize the special state of the `MenuItem` component:
```css
.MenuItem {
color: black;
}
/* Increase the specificity */
.MenuItem.Mui-selected {
color: blue;
}
```
```jsx
<MenuItem selected className="MenuItem">
```
If you'd like to learn more about this topic, we recommend checking out [the MDN Web Docs on CSS Specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascade/Specificity).
#### Why do I need to increase specificity to override one component state?
CSS pseudo-classes have a high level of specificity.
For consistency with native elements, Material UI's state classes have the same level of specificity as CSS pseudo-classes, making it possible to target an individual component's state.
#### What custom state classes are available in Material UI?
You can rely on the following [global class names](https://v6.mui.com/system/styles/advanced/#class-names) generated by Material UI:
| State | Global class name |
| :------------ | :------------------ |
| active | `.Mui-active` |
| checked | `.Mui-checked` |
| completed | `.Mui-completed` |
| disabled | `.Mui-disabled` |
| error | `.Mui-error` |
| expanded | `.Mui-expanded` |
| focus visible | `.Mui-focusVisible` |
| focused | `.Mui-focused` |
| readOnly | `.Mui-readOnly` |
| required | `.Mui-required` |
| selected | `.Mui-selected` |
:::error
Never apply styles directly to state class names. This will impact all components with unclear side-effects. Always target a state class together with a component.
:::
```css
/* ❌ NOT OK */
.Mui-error {
color: red;
}
/* ✅ OK */
.MuiOutlinedInput-root.Mui-error {
color: red;
}
```
## 2. Reusable component
To reuse the same overrides in different locations across your application, create a reusable component using the [`styled()`](/system/styled/) utility:
{{"demo": "StyledCustomization.js", "defaultCodeOpen": true}}
### Dynamic overrides
The `styled()` utility lets you add dynamic styles based on a component's props.
You can do this with **dynamic CSS** or **CSS variables**.
#### Dynamic CSS
:::warning
If you are using TypeScript, you will need to update the prop's types of the new component.
:::
{{"demo": "DynamicCSS.js", "defaultCodeOpen": false}}
```tsx
import * as React from 'react';
import { styled } from '@mui/material/styles';
import Slider, { SliderProps } from '@mui/material/Slider';
interface StyledSliderProps extends SliderProps {
success?: boolean;
}
const StyledSlider = styled(Slider, {
shouldForwardProp: (prop) => prop !== 'success',
})<StyledSliderProps>(({ success, theme }) => ({
...(success &&
{
// the overrides added when the new prop is used
}),
}));
```
#### CSS variables
{{"demo": "DynamicCSSVariables.js"}}
## 3. Global theme overrides
Material UI provides theme tools for managing style consistency between all components across your user interface.
Visit the [Component theming customization](/material-ui/customization/theme-components/) page for more details.
## 4. Global CSS override
To add global baseline styles for some of the HTML elements, use the `GlobalStyles` component.
Here is an example of how you can override styles for the `h1` elements:
{{"demo": "GlobalCssOverride.js", "iframe": true, "height": 100}}
The `styles` prop in the `GlobalStyles` component supports a callback in case you need to access the theme.
{{"demo": "GlobalCssOverrideTheme.js", "iframe": true, "height": 100}}
If you are already using the [CssBaseline](/material-ui/react-css-baseline/) component for setting baseline styles, you can also add these global styles as overrides for this component. Here is how you can achieve the same by using this approach.
{{"demo": "OverrideCssBaseline.js", "iframe": true, "height": 100}}
The `styleOverrides` key in the `MuiCssBaseline` component slot also supports callback from which you can access the theme. Here is how you can achieve the same by using this approach.
{{"demo": "OverrideCallbackCssBaseline.js", "iframe": true, "height": 100}}
:::success
It is a good practice to hoist the `<GlobalStyles />` to a static constant, to avoid rerendering. This will ensure that the `<style>` tag generated would not recalculate on each render.
:::
```diff
import * as React from 'react';
import GlobalStyles from '@mui/material/GlobalStyles';
+const inputGlobalStyles = <GlobalStyles styles={...} />;
function Input(props) {
return (
<React.Fragment>
- <GlobalStyles styles={...} />
+ {inputGlobalStyles}
<input {...props} />
</React.Fragment>
)
}
```

View File

@@ -0,0 +1,32 @@
import Box from '@mui/material/Box';
import PropTypes from 'prop-types';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
function PopperComponent(props) {
const { disablePortal, anchorEl, open, ...other } = props;
return <div {...other} />;
}
PopperComponent.propTypes = {
anchorEl: PropTypes.any,
disablePortal: PropTypes.bool,
open: PropTypes.bool.isRequired,
};
export default function OverridingInternalSlot() {
return (
<Box
sx={{ display: 'flex', flexDirection: 'column', width: 320, minHeight: 220 }}
>
<Autocomplete
open
options={['🆘 Need help', '✨ Improvement', '🚀 New feature', '🐛 Bug fix']}
renderInput={(params) => <TextField {...params} />}
slots={{
popper: PopperComponent,
}}
/>
</Box>
);
}

View File

@@ -0,0 +1,31 @@
import Box from '@mui/material/Box';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
interface PopperComponentProps {
anchorEl?: any;
disablePortal?: boolean;
open: boolean;
}
function PopperComponent(props: PopperComponentProps) {
const { disablePortal, anchorEl, open, ...other } = props;
return <div {...other} />;
}
export default function OverridingInternalSlot() {
return (
<Box
sx={{ display: 'flex', flexDirection: 'column', width: 320, minHeight: 220 }}
>
<Autocomplete
open
options={['🆘 Need help', '✨ Improvement', '🚀 New feature', '🐛 Bug fix']}
renderInput={(params) => <TextField {...params} />}
slots={{
popper: PopperComponent,
}}
/>
</Box>
);
}

View File

@@ -0,0 +1,8 @@
<Autocomplete
open
options={['🆘 Need help', '✨ Improvement', '🚀 New feature', '🐛 Bug fix']}
renderInput={(params) => <TextField {...params} />}
slots={{
popper: PopperComponent,
}}
/>

View File

@@ -0,0 +1,14 @@
import Button from '@mui/material/Button';
export default function OverridingRootSlot() {
return (
<Button
component="a"
href="https://mui.com/about/"
target="_blank"
rel="noopener"
>
About us
</Button>
);
}

View File

@@ -0,0 +1,14 @@
import Button from '@mui/material/Button';
export default function OverridingRootSlot() {
return (
<Button
component="a"
href="https://mui.com/about/"
target="_blank"
rel="noopener"
>
About us
</Button>
);
}

View File

@@ -0,0 +1,8 @@
<Button
component="a"
href="https://mui.com/about/"
target="_blank"
rel="noopener"
>
About us
</Button>

View File

@@ -0,0 +1,133 @@
# Overriding component structure
<p class="description">Learn how to override the default DOM structure of Material UI components.</p>
Material UI components are designed to suit the widest possible range of use cases, but you may occasionally need to change how a component's structure is rendered in the DOM.
To understand how to do this, it helps to know a bit about how the API design has evolved over time, and to have an accurate mental model of the components themselves.
## Context
Prior to Material UI v6, it was not possible to override the structure of most components in the library.
Some components had `*Props` props that allowed you to pass props to a specific slot, but this pattern was not applied consistently.
In v6, those props were deprecated in favor of the `slots` and `slotProps` props, which allow for more granular control over the structure of a component and make the API more consistent across the library.
## The mental model
A component's structure is determined by the elements that fill that component's **slots**.
Slots are most commonly filled by HTML tags, but may also be filled by React components.
All components contain a root slot that defines their primary node in the DOM tree; more complex components also contain additional interior slots named after the elements they represent.
:::info
To see the available slots for a component, refer to the slots sections of the respective component API documentation.
:::
All _non-utility_ Material UI components accept two props for overriding their rendered HTML structure:
- `component`—to override the root slot
- `slots`—to replace any interior slots (when present) as well as the root
Additionally, you can pass custom props to interior slots using `slotProps`.
## The root slot
The root slot represents the component's outermost element. It is filled by a styled component with an appropriate HTML element.
For example, the [Button's](/material-ui/react-button/) root slot is a `<button>` element.
This component _only_ has a root slot; more complex components may have additional [interior slots](#interior-slots).
### The component prop
Use the `component` prop to override a component's root slot.
The demo below shows how to replace the Button's `<button>` tag with a `<a>` to create a link button:
{{"demo": "OverridingRootSlot.js"}}
:::info
The `href`, `target`, and `rel` props are specific to `<a>` tags.
When using the `component` prop, be sure to add the appropriate attributes that correspond to the element you're inserting.
:::
## Interior slots
Complex components are composed of one or more interior slots in addition to the root.
These slots are often (but not necessarily) nested within the root.
For example, the [Autocomplete](/material-ui/react-autocomplete/) is composed of a root `<div>` that houses several interior slots named for the elements they represent: input, startDecorator, endDecorator, clearIndicator, popupIndicator, and more.
### The slots prop
Use the `slots` prop to replace a component's interior slots.
The example below shows how to replace the popper slot in the [Autocomplete](/material-ui/react-autocomplete/) component to remove the popup functionality:
{{"demo": "OverridingInternalSlot.js"}}
### The slotProps prop
The `slotProps` prop is an object that contains the props for all slots within a component.
You can use it to define additional custom props to pass to a component's interior slots.
For example, the code snippet below shows how to add a custom `data-testid` to the popper slot of the [Autocomplete](/material-ui/react-autocomplete/) component:
```jsx
<Autocomplete slotProps={{ popper: { 'data-testid': 'my-popper' } }} />
```
All additional props placed on the primary component are also propagated into the root slot (just as if they were placed in `slotProps.root`).
These two examples are equivalent:
```jsx
<Autocomplete id="badge1">
```
```jsx
<Autocomplete slotProps={{ root: { id: 'badge1' } }}>
```
:::warning
If both `slotProps.root` and additional props have the same keys but different values, the `slotProps.root` props will take precedence.
This does not apply to classes or the `style` prop—they will be merged instead.
:::
### Type safety
The `slotProps` prop is not dynamically typed based on the custom `slots` prop, so if the custom slot has a different type than the default slot, you have to cast the type to avoid TypeScript errors and use `satisfies` (available in TypeScript 4.9) to ensure type safety for the custom slot.
The example below shows how to customize the `img` slot of the [Avatar](/material-ui/react-avatar/) component using [Next.js Image](https://nextjs.org/docs/app/api-reference/components/image) component:
```tsx
import Image, { ImageProps } from 'next/image';
import Avatar, { AvatarProps } from '@mui/material/Avatar';
<Avatar
slots={{
img: Image,
}}
slotProps={
{
img: {
src: 'https://example.com/image.jpg',
alt: 'Image',
width: 40,
height: 40,
blurDataURL: 'data:image/png;base64',
} satisfies ImageProps,
} as AvatarProps['slotProps']
}
/>;
```
## Best practices
Use the `component` or `slotProps.{slot}.component` prop when you need to override the element while preserving the styles of the slot.
Use the `slots` prop when you need to replace the slot's styles and functionality with your custom component.
Overriding with `component` lets you apply the attributes of that element directly to the root.
For instance, if you override the Button's root with an `<li>` tag, you can add the `<li>` attribute `value` directly to the component.
If you did the same with `slots.root`, you would need to place this attribute on the `slotProps.root` object in order to avoid a TypeScript error.
Be mindful of your rendered DOM structure when overriding the slots of more complex components.
You can easily break the rules of semantic and accessible HTML if you deviate too far from the default structure—for instance, by unintentionally nesting block-level elements inside of inline elements.

View File

@@ -0,0 +1,41 @@
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { blue } from '@mui/material/colors';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
const theme = createTheme({
palette: {
primary: {
light: blue[300],
main: blue[500],
dark: blue[700],
darker: blue[900],
},
},
});
export default function AddingColorTokens() {
return (
<ThemeProvider theme={theme}>
<Stack direction="row" sx={{ gap: 1 }}>
<Stack sx={{ alignItems: 'center' }}>
<Typography variant="body2">light</Typography>
<Box sx={{ bgcolor: `primary.light`, width: 40, height: 20 }} />
</Stack>
<Stack sx={{ alignItems: 'center' }}>
<Typography variant="body2">main</Typography>
<Box sx={{ bgcolor: `primary.main`, width: 40, height: 20 }} />
</Stack>
<Stack sx={{ alignItems: 'center' }}>
<Typography variant="body2">dark</Typography>
<Box sx={{ bgcolor: `primary.dark`, width: 40, height: 20 }} />
</Stack>
<Stack sx={{ alignItems: 'center' }}>
<Typography variant="body2">darker</Typography>
<Box sx={{ bgcolor: `primary.darker`, width: 40, height: 20 }} />
</Stack>
</Stack>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,51 @@
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { blue } from '@mui/material/colors';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
declare module '@mui/material/styles' {
interface PaletteColor {
darker?: string;
}
interface SimplePaletteColorOptions {
darker?: string;
}
}
const theme = createTheme({
palette: {
primary: {
light: blue[300],
main: blue[500],
dark: blue[700],
darker: blue[900],
},
},
});
export default function AddingColorTokens() {
return (
<ThemeProvider theme={theme}>
<Stack direction="row" sx={{ gap: 1 }}>
<Stack sx={{ alignItems: 'center' }}>
<Typography variant="body2">light</Typography>
<Box sx={{ bgcolor: `primary.light`, width: 40, height: 20 }} />
</Stack>
<Stack sx={{ alignItems: 'center' }}>
<Typography variant="body2">main</Typography>
<Box sx={{ bgcolor: `primary.main`, width: 40, height: 20 }} />
</Stack>
<Stack sx={{ alignItems: 'center' }}>
<Typography variant="body2">dark</Typography>
<Box sx={{ bgcolor: `primary.dark`, width: 40, height: 20 }} />
</Stack>
<Stack sx={{ alignItems: 'center' }}>
<Typography variant="body2">darker</Typography>
<Box sx={{ bgcolor: `primary.darker`, width: 40, height: 20 }} />
</Stack>
</Stack>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,48 @@
import { createTheme, ThemeProvider, useTheme } from '@mui/material/styles';
import PropTypes from 'prop-types';
import Button from '@mui/material/Button';
import { Stack } from '@mui/system';
const defaultContrastThresholdTheme = createTheme({});
const highContrastThresholdTheme = createTheme({
palette: {
contrastThreshold: 4.5,
},
});
function ContrastShowcase(props) {
const { title } = props;
const theme = useTheme();
return (
<Stack sx={{ gap: 1, alignItems: 'center' }}>
<span>
<b>{title}</b>
</span>
<span>{theme.palette.contrastThreshold}:1</span>
<Stack direction="row" sx={{ gap: 1 }}>
<Button variant="contained" color="warning">
Warning
</Button>
</Stack>
</Stack>
);
}
ContrastShowcase.propTypes = {
title: PropTypes.string.isRequired,
};
export default function ContrastThreshold() {
return (
<Stack direction="row" sx={{ gap: 4 }}>
<ThemeProvider theme={defaultContrastThresholdTheme}>
<ContrastShowcase title="Default contrast threshold" />
</ThemeProvider>
<ThemeProvider theme={highContrastThresholdTheme}>
<ContrastShowcase title="Higher contrast threshold" />
</ThemeProvider>
</Stack>
);
}

View File

@@ -0,0 +1,43 @@
import { createTheme, ThemeProvider, useTheme } from '@mui/material/styles';
import Button from '@mui/material/Button';
import { Stack } from '@mui/system';
const defaultContrastThresholdTheme = createTheme({});
const highContrastThresholdTheme = createTheme({
palette: {
contrastThreshold: 4.5,
},
});
function ContrastShowcase(props: { title: string }) {
const { title } = props;
const theme = useTheme();
return (
<Stack sx={{ gap: 1, alignItems: 'center' }}>
<span>
<b>{title}</b>
</span>
<span>{theme.palette.contrastThreshold}:1</span>
<Stack direction="row" sx={{ gap: 1 }}>
<Button variant="contained" color="warning">
Warning
</Button>
</Stack>
</Stack>
);
}
export default function ContrastThreshold() {
return (
<Stack direction="row" sx={{ gap: 4 }}>
<ThemeProvider theme={defaultContrastThresholdTheme}>
<ContrastShowcase title="Default contrast threshold" />
</ThemeProvider>
<ThemeProvider theme={highContrastThresholdTheme}>
<ContrastShowcase title="Higher contrast threshold" />
</ThemeProvider>
</Stack>
);
}

View File

@@ -0,0 +1,6 @@
<ThemeProvider theme={defaultContrastThresholdTheme}>
<ContrastShowcase title="Default contrast threshold" />
</ThemeProvider>
<ThemeProvider theme={highContrastThresholdTheme}>
<ContrastShowcase title="Higher contrast threshold" />
</ThemeProvider>

View File

@@ -0,0 +1,23 @@
import { createTheme, ThemeProvider } from '@mui/material/styles';
import Button from '@mui/material/Button';
const theme = createTheme({
palette: {
neutral: {
light: '#838fa2',
main: '#64748b',
dark: '#465161',
contrastText: '#fff',
},
},
});
export default function CustomColor() {
return (
<ThemeProvider theme={theme}>
<Button color="neutral" variant="contained">
neutral
</Button>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,41 @@
import { createTheme, ThemeProvider } from '@mui/material/styles';
import Button from '@mui/material/Button';
const theme = createTheme({
palette: {
neutral: {
light: '#838fa2',
main: '#64748b',
dark: '#465161',
contrastText: '#fff',
},
},
});
declare module '@mui/material/styles' {
interface Palette {
neutral: Palette['primary'];
}
// allow configuration using `createTheme()`
interface PaletteOptions {
neutral?: PaletteOptions['primary'];
}
}
// @babel-ignore-comment-in-output Update the Button's color prop options
declare module '@mui/material/Button' {
interface ButtonPropsColorOverrides {
neutral: true;
}
}
export default function CustomColor() {
return (
<ThemeProvider theme={theme}>
<Button color="neutral" variant="contained">
neutral
</Button>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,5 @@
<ThemeProvider theme={theme}>
<Button color="neutral" variant="contained">
neutral
</Button>
</ThemeProvider>

View File

@@ -0,0 +1,77 @@
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import { useTheme, rgbToHex, styled } from '@mui/material/styles';
const Group = styled(Typography)(({ theme }) => ({
marginTop: theme.spacing(3),
}));
const Color = styled(Grid)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
'& div:first-of-type': {
width: theme.spacing(6),
height: theme.spacing(6),
marginRight: theme.spacing(1),
borderRadius: theme.shape.borderRadius,
boxShadow: 'inset 0 2px 4px 0 rgba(0, 0, 0, .06)',
},
}));
export default function Intentions() {
const theme = useTheme();
const item = (color, name) => (
<Color size={{ xs: 12, sm: 6, md: 4 }}>
<div style={{ backgroundColor: color }} />
<div>
<Typography variant="body2">{name}</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{rgbToHex(color)}
</Typography>
</div>
</Color>
);
return (
<Box sx={{ width: '100%' }}>
<Group gutterBottom>Primary</Group>
<Grid container spacing={2}>
{item(theme.palette.primary.light, 'palette.primary.light')}
{item(theme.palette.primary.main, 'palette.primary.main')}
{item(theme.palette.primary.dark, 'palette.primary.dark')}
</Grid>
<Group gutterBottom>Secondary</Group>
<Grid container spacing={2}>
{item(theme.palette.secondary.light, 'palette.secondary.light')}
{item(theme.palette.secondary.main, 'palette.secondary.main')}
{item(theme.palette.secondary.dark, 'palette.secondary.dark')}
</Grid>
<Group gutterBottom>Error</Group>
<Grid container spacing={2}>
{item(theme.palette.error.light, 'palette.error.light')}
{item(theme.palette.error.main, 'palette.error.main')}
{item(theme.palette.error.dark, 'palette.error.dark')}
</Grid>
<Group gutterBottom>Warning</Group>
<Grid container spacing={2}>
{item(theme.palette.warning.light, 'palette.warning.light')}
{item(theme.palette.warning.main, 'palette.warning.main')}
{item(theme.palette.warning.dark, 'palette.warning.dark')}
</Grid>
<Group gutterBottom>Info</Group>
<Grid container spacing={2}>
{item(theme.palette.info.light, 'palette.info.light')}
{item(theme.palette.info.main, 'palette.info.main')}
{item(theme.palette.info.dark, 'palette.info.dark')}
</Grid>
<Group gutterBottom>Success</Group>
<Grid container spacing={2}>
{item(theme.palette.success.light, 'palette.success.light')}
{item(theme.palette.success.main, 'palette.success.main')}
{item(theme.palette.success.dark, 'palette.success.dark')}
</Grid>
</Box>
);
}

Some files were not shown because too many files have changed in this diff Show More