Material-UI System
CSS utilities for rapidly creating custom design.
Material-UI comes with dozens or ready-to-use components in the core. These components are an incredible starting point but when it comes to making your site stand out with a custom design, it can be simpler to start from an unstyled state. Introducing the system:
The system lets you quickly build custom UI components leveraging the values defined in your theme.
Demo
(Resize the window to see the responsive breakpoints)
import * as React from 'react';
import Box from '@material-ui/core/Box';
import { alpha } from '@material-ui/core/styles';
import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline';
function Img(props) {
return <Box component="img" {...props} />;
}
export default function Demo() {
return (
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
alignItems: 'center',
bgcolor: 'background.paper',
overflow: 'hidden',
borderRadius: '12px',
boxShadow: 1,
fontWeight: 'fontWeightBold',
}}
>
<Img
sx={{ width: '100%', maxWidth: { xs: 350, md: 250 } }}
alt="The house from the offer."
src="https://images.unsplash.com/photo-1512917774080-9991f1c4c750?auto=format&w=400&dpr=2"
/>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: { xs: 'center', md: 'flex-start' },
m: 3,
minWidth: { md: 350 },
}}
>
<Box component="span" sx={{ fontSize: 16, mt: 1 }}>
123 Main St, Phoenix AZ
</Box>
<Box component="span" sx={{ color: 'primary.main', fontSize: 22 }}>
$280,000 — $310,000
</Box>
<Box
sx={{
mt: 1.5,
p: 0.5,
backgroundColor: (theme) => alpha(theme.palette.primary.main, 0.1),
borderRadius: '5px',
color: 'primary.main',
fontWeight: 'fontWeightMedium',
display: 'flex',
fontSize: 12,
alignItems: 'center',
'& svg': {
fontSize: 21,
mr: 0.5,
},
}}
>
<ErrorOutlineIcon />
CONFIDENCE SCORE 85%
</Box>
</Box>
</Box>
);
}
Installation
// with npm
npm install @material-ui/system
// with yarn
yarn add @material-ui/system
Why use the system?
Compare how the same stat component can be built with two different APIs.
- ❌ using the styled-components's API:
const StatWrapper = styled('div')(
({ theme }) => `
background-color: ${theme.palette.background.paper};
box-shadow: ${theme.shadows[1]};
border-radius: ${theme.shape.borderRadius}px;
padding: ${theme.spacing(2)};
min-width: 300px;
`,
);
const StatHeader = styled('div')(
({ theme }) => `
color: ${theme.palette.text.secondary};
`,
);
const StyledTrend = styled(TrendingUpIcon)(
({ theme }) => `
color: ${theme.palette.success.dark};
font-size: 16px;
vertical-alignment: sub;
`,
);
const StatValue = styled('div')(
({ theme }) => `
color: ${theme.palette.text.primary};
font-size: 34px;
font-weight: ${theme.typography.fontWeightMedium};
`,
);
const StatDiff = styled('div')(
({ theme }) => `
color: ${theme.palette.success.dark};
display: inline;
font-weight: ${theme.typography.fontWeightMedium};
margin-left: ${theme.spacing(0.5)};
margin-right: ${theme.spacing(0.5)};
`,
);
const StatPrevious = styled('div')(
({ theme }) => `
color: ${theme.palette.text.secondary};
display: inline;
font-size: 12px;
`,
);
return (
<StatWrapper>
<StatHeader>Sessions</StatHeader>
<StatValue>98.3 K</StatValue>
<StyledTrend />
<StatDiff>18.77%</StatDiff>
<StatPrevious>vs last week</StatPrevious>
</StatWrapper>
);
- ✅ using the system:
<Box
sx={{
bgcolor: 'background.paper',
boxShadow: 1,
borderRadius: 'borderRadius',
p: 2,
minWidth: 300,
}}
>
<Box
sx={{
color: 'text.secondary',
}}
>
Sessions
</Box>
<Box
sx={{
color: 'text.primary',
fontSize: 34,
fontWeight: 'fontWeightMedium',
}}
>
98.3 K
</Box>
<Box
component={TrendingUpIcon}
sx={{
color: 'success.dark',
fontSize: 16,
verticalAlign: 'sub',
}}
/>
<Box
sx={{
color: 'success.dark',
display: 'inline',
fontWeight: 'fontWeightMedium',
mx: 0.5,
}}
>
18.77%
</Box>
<Box
sx={{
color: 'text.secondary',
display: 'inline',
fontSize: 12,
}}
>
vs last week
</Box>
</Box>
Problem solved
The system focus on solving 3 main problems:
1. Switching context wastes time.
There's no need to constantly jump between the usage of the styled components and where they are defined. With the system, those descriptions are right where you need them.
2. Naming things is hard.
Have you ever found yourself struggling to find a good name for a styled component? The system maps the styles directly to the element. All you have to do is worry about actual style properties.
3. Enforcing consistency in UIs is hard.
This is especially true when more than one person is building the application, as there has to be some coordination amongst members of the team regarding the choice of design tokens and how they are used, what parts of the theme structure should be used with what CSS properties, and so on.
The system provides direct access to the value in the theme. It makes it easier to design with constraints.
The sx
prop
The sx
prop, as the main part of the system, solves these problems by providing a fast & simple way of applying the correct design tokens for specific CSS properties directly to a React element. The demo above shows how it can be used to create a one-off design.
This prop provides a superset of CSS that maps values directly from the theme, depending on the CSS property used. Also, it allows a simple way of defining responsive values that correspond to the breakpoints defined in the theme.
When to use it?
- styled-components: the API is great to build components that need to support a wide variety of contexts. These components are used in many different parts of the application and support different combinations of props.
sx
prop: the API is great to apply one-off styles. It's called "utility" for this reason.
Performance tradeoff
The system relies on CSS-in-JS. It works with both emotion and styled-components.
Pros:
- 📚 It allows a lot of flexibility in the API. The
sx
prop supports a superset of CSS. There is no need to learn CSS twice. You are set once you have learn the standardized CSS syntax, it's safe, it hasn't changed for a decade. Then, you can optionally learn the shorthands if you value the save of time they bring. - 📦 Auto-purge. Only the used CSS on the page is sent to the client. The initial bundle size cost is fixed. It's not growing with the number of used CSS properties. You pay the cost of @emotion/react and @material-ui/system. It cost around ~15 kB gzipped. If you are already using the core components, it comes with no extra overhead.
Cons:
The runtime performance take a hit.
Benchmark case Code snippet Time normalized a. Render 1,000 primitives <div className="">
100ms b. Render 1,000 components <Div>
110ms c. Render 1,000 styled components <StyledDiv>
160ms d. Render 1,000 Box <Box sx={}>
270ms Head to the benchmark folder for a reproduction of these metrics.
We believe that for most applications, it's fast enough. There are simple workarounds when performance becomes critical. For instance, when rendering a list with many items, you can use a CSS child selector to have a single "style injection" point (using d. for the wrapper and a. for each item).
Usage
Design tokens in the theme
You can explore the System properties page to discover how the different CSS (and custom) properties are mapped to the theme keys.
Shorthands
There are lots of shorthands available for the CSS properties. These are documented in the next pages, for instance, the spacing. Here is an example leveraging them:
<Box
sx={{
boxShadow: 1, // theme.shadows[1]
color: 'primary.main', // theme.palette.primary.main
m: 1, // margin: theme.spacing(1)
p: {
sx: 1, // [theme.breakpoints.up('sx')]: : { padding: theme.spacing(1) }
},
zIndex: 'tooltip', // theme.zIndex.tooltip
}}
>
These shorthands are optional, they are great to save time when writing styles but it can be overwhelming to learn new custom APIs. You might want to skip this part and bet on CSS, it has been standardized for decades, head to the next section.
Superset of CSS
As part of the prop, you can use any regular CSS too: child or pseudo-selectors, media queries, raw CSS values, etc. Here are a few examples:
Using pseudo selectors:
<Box sx={{ // some styles ":hover": { boxShadow: 6, }, }} >
Using media queries:
<Box sx={{ // some styles '@media print': { width: 300, }, }} >
Using nested selector:
<Box sx={{ // some styles '& .ChildSelector': { bgcolor: 'primary.main', }, }} >
Responsive values
If you would like to have responsive values for a CSS property, you can use the breakpoints shorthand syntax. There are two ways of defining the breakpoints:
1. Breakpoints as an object
The first option for defining breakpoints is to define them as an object, using the breakpoints as keys. Here is the previous example again, using the object syntax.
<Box sx={{ width: { sx: 100, sm: 200, md: 300, lg: 400, xl: 500 } }}>
This box has a responsive width.
</Box>
2. Breakpoints as an array
The second option is to define your breakpoints as an array, from the smallest to the largest breakpoint.
<Box sx={{ width: [100, 200, 300] }}>
This box has a responsive width.
</Box>
⚠️ This option is only recommended when the theme has a limited number of breakpoints, e.g. 3.
Prefer the object API if you have more breakpoints. For instance, the default theme of Material-UI has 5.
You can skip breakpoints with the null
value:
<Box sx={{ width: [null, null, 300] }}>This box has a responsive width.</Box>
Custom breakpoints
You can also specify your own custom breakpoints, and use them as keys when defining the breakpoints object. Here is an example of how to do that.
import * as React from 'react';
import Box from '@material-ui/core/Box';
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
const theme = createMuiTheme({
breakpoints: {
values: {
tablet: 640,
laptop: 1024,
desktop: 1280,
},
},
});
export default function CustomBreakpoints() {
return (
<ThemeProvider theme={theme}>
<Box
sx={{
width: {
tablet: 100,
laptop: 300,
desktop: 500,
},
}}
>
This box has a responsive width
</Box>
</ThemeProvider>
);
}
If you are using TypeScript, you will also need to use module augmentation for the theme to accept the above values.
declare module '@material-ui/core/styles/createBreakpoints' {
interface BreakpointOverrides {
xs: false; // removes the `xs` breakpoint
sm: false;
md: false;
lg: false;
xl: false;
tablet: true; // adds the `tablet` breakpoint
laptop: true;
desktop: true;
}
}
Theme getter
If you wish to use the theme for a CSS property that is not supported natively by the system, you can use a function as the value, in which you can access the theme object.
<Box
sx={{
p: 1,
border: 1,
borderColor: (theme) => theme.palette.primary.main,
}}
>
Border color with theme value.
</Box>
Implementations
The sx
prop can be used in four different locations:
1. Core components
All core Material-UI components will support the sx
prop.
2. Box
Box
is a lightweight component that gives access to the sx
prop, and can be used as a utility component, and as a wrapper for other components.
It renders a <div>
element by default.
3. Custom components
In addition to Material-UI components, you can add the sx
prop to your custom components too, by using the experimentalStyled
utility from @material-ui/core/styles
.
import { experimentalStyled as styled } from '@material-ui/core/styles';
const Div = styled('div')``;
4. Any element with the babel plugin
TODO #23220.