diff --git a/benchmark/browser/scenarios/sx-prop-box-material-ui/index.js b/benchmark/browser/scenarios/sx-prop-box-material-ui/index.js
new file mode 100644
index 00000000000000..1988668b132ec3
--- /dev/null
+++ b/benchmark/browser/scenarios/sx-prop-box-material-ui/index.js
@@ -0,0 +1,26 @@
+import * as React from 'react';
+import Box from '@material-ui/core/Box';
+
+export default function BoxSxPropMaterialUI() {
+ return (
+
+ {new Array(1000).fill().map(() => (
+ theme.palette.secondary.dark,
+ },
+ }}
+ >
+ material-ui
+
+ ))}
+
+ );
+}
diff --git a/benchmark/browser/scripts/benchmark.js b/benchmark/browser/scripts/benchmark.js
index 68e34047982955..da4c40620421f1 100644
--- a/benchmark/browser/scripts/benchmark.js
+++ b/benchmark/browser/scripts/benchmark.js
@@ -120,6 +120,7 @@ async function run() {
await runMeasures(browser, 'Chakra-UI box component', './box-chakra-ui/index.js', 10);
await runMeasures(browser, 'Theme-UI box sx prop', './sx-prop-box-theme-ui/index.js', 10);
await runMeasures(browser, 'Theme-UI div sx prop', './sx-prop-div-theme-ui/index.js', 10);
+ await runMeasures(browser, 'Material-UI box sx prop', './sx-prop-box-material-ui/index.js', 10);
} finally {
await Promise.all([browser.close(), server.close()]);
}
diff --git a/docs/src/pages/components/box/BoxClone.js b/docs/src/pages/components/box/BoxClone.js
new file mode 100644
index 00000000000000..446d8fa4ab11b2
--- /dev/null
+++ b/docs/src/pages/components/box/BoxClone.js
@@ -0,0 +1,11 @@
+import * as React from 'react';
+import Button from '@material-ui/core/Button';
+import Box from '@material-ui/core/Box';
+
+export default function BoxClone() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/src/pages/components/box/BoxClone.tsx b/docs/src/pages/components/box/BoxClone.tsx
new file mode 100644
index 00000000000000..446d8fa4ab11b2
--- /dev/null
+++ b/docs/src/pages/components/box/BoxClone.tsx
@@ -0,0 +1,11 @@
+import * as React from 'react';
+import Button from '@material-ui/core/Button';
+import Box from '@material-ui/core/Box';
+
+export default function BoxClone() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/src/pages/components/box/BoxComponent.js b/docs/src/pages/components/box/BoxComponent.js
new file mode 100644
index 00000000000000..ce331b43e9e107
--- /dev/null
+++ b/docs/src/pages/components/box/BoxComponent.js
@@ -0,0 +1,11 @@
+import * as React from 'react';
+import Box from '@material-ui/core/Box';
+import Button from '@material-ui/core/Button';
+
+export default function BoxComponent() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/src/pages/components/box/BoxComponent.tsx b/docs/src/pages/components/box/BoxComponent.tsx
new file mode 100644
index 00000000000000..ce331b43e9e107
--- /dev/null
+++ b/docs/src/pages/components/box/BoxComponent.tsx
@@ -0,0 +1,11 @@
+import * as React from 'react';
+import Box from '@material-ui/core/Box';
+import Button from '@material-ui/core/Button';
+
+export default function BoxComponent() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/src/pages/components/box/BoxRenderProps.js b/docs/src/pages/components/box/BoxRenderProps.js
new file mode 100644
index 00000000000000..071fe19fe50074
--- /dev/null
+++ b/docs/src/pages/components/box/BoxRenderProps.js
@@ -0,0 +1,11 @@
+import * as React from 'react';
+import Button from '@material-ui/core/Button';
+import Box from '@material-ui/core/Box';
+
+export default function BoxClone() {
+ return (
+
+ {(props) => }
+
+ );
+}
diff --git a/docs/src/pages/components/box/BoxRenderProps.tsx b/docs/src/pages/components/box/BoxRenderProps.tsx
new file mode 100644
index 00000000000000..6345cc4b077286
--- /dev/null
+++ b/docs/src/pages/components/box/BoxRenderProps.tsx
@@ -0,0 +1,11 @@
+import * as React from 'react';
+import Button from '@material-ui/core/Button';
+import Box from '@material-ui/core/Box';
+
+export default function BoxClone() {
+ return (
+
+ {(props: { className: string }) => }
+
+ );
+}
diff --git a/docs/src/pages/components/box/BoxSx.js b/docs/src/pages/components/box/BoxSx.js
new file mode 100644
index 00000000000000..ebe455fd5bc221
--- /dev/null
+++ b/docs/src/pages/components/box/BoxSx.js
@@ -0,0 +1,18 @@
+import * as React from 'react';
+import Box from '@material-ui/core/Box';
+
+export default function BoxSx() {
+ return (
+
+ );
+}
diff --git a/docs/src/pages/components/box/BoxSx.tsx b/docs/src/pages/components/box/BoxSx.tsx
new file mode 100644
index 00000000000000..ebe455fd5bc221
--- /dev/null
+++ b/docs/src/pages/components/box/BoxSx.tsx
@@ -0,0 +1,18 @@
+import * as React from 'react';
+import Box from '@material-ui/core/Box';
+
+export default function BoxSx() {
+ return (
+
+ );
+}
diff --git a/docs/src/pages/components/box/box.md b/docs/src/pages/components/box/box.md
index 1d39a7347ddbb6..45d3bf682592f3 100644
--- a/docs/src/pages/components/box/box.md
+++ b/docs/src/pages/components/box/box.md
@@ -22,11 +22,7 @@ The Box component wraps your component.
It creates a new DOM element, a `
` by default that can be changed with the `component` prop.
Let's say you want to use a `
` instead:
-```jsx
-
-
-
-```
+{{"demo": "pages/components/box/BoxComponent.js", "defaultCodeOpen": true }}
This works great when the changes can be isolated to a new DOM element.
For instance, you can change the margin this way.
@@ -40,23 +36,23 @@ To workaround the problem, you have two options:
The Box component has a `clone` prop to enable the use of the clone element method of React.
-```jsx
-
-
-
-```
+{{"demo": "pages/components/box/BoxClone.js", "defaultCodeOpen": true }}
2. Use render props
The Box children accepts a render props function. You can pull out the `className`.
-```jsx
-{(props) => }
-```
+{{"demo": "pages/components/box/BoxRenderProps.js", "defaultCodeOpen": true }}
> ⚠️ The CSS specificity relies on the import order.
> If you want the guarantee that the wrapped component's style will be overridden, you need to import the Box last.
+## The sx prop
+
+Sometimes, the props on the Box component are not enough to style the component. To solve this, `Box` supports the `sx` prop. This allows you to specify any CSS rules you want, in addition to the ones already available using system props. Here is an example of how you can use it:
+
+{{"demo": "pages/components/box/BoxSx.js", "defaultCodeOpen": true }}
+
## API
```jsx
diff --git a/packages/material-ui-system/src/breakpoints.js b/packages/material-ui-system/src/breakpoints.js
index 029896503ce07b..2ad8b8233e8a2d 100644
--- a/packages/material-ui-system/src/breakpoints.js
+++ b/packages/material-ui-system/src/breakpoints.js
@@ -36,7 +36,13 @@ export function handleBreakpoints(props, propValue, styleFromPropValue) {
if (typeof propValue === 'object') {
const themeBreakpoints = props.theme.breakpoints || defaultBreakpoints;
return Object.keys(propValue).reduce((acc, breakpoint) => {
- acc[themeBreakpoints.up(breakpoint)] = styleFromPropValue(propValue[breakpoint]);
+ // key is breakpoint
+ if (Object.keys(themeBreakpoints.values || values).indexOf(breakpoint) !== -1) {
+ acc[themeBreakpoints.up(breakpoint)] = styleFromPropValue(propValue[breakpoint]);
+ } else {
+ const cssKey = breakpoint;
+ acc[cssKey] = propValue[cssKey];
+ }
return acc;
}, {});
}
diff --git a/packages/material-ui-system/src/index.d.ts b/packages/material-ui-system/src/index.d.ts
index 01852b04881085..441052f8f41f7f 100644
--- a/packages/material-ui-system/src/index.d.ts
+++ b/packages/material-ui-system/src/index.d.ts
@@ -30,6 +30,12 @@ export type BordersProps = PropsFor;
// breakpoints.js
type DefaultBreakPoints = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+export function handleBreakpoints(
+ props: Props,
+ propValue: any,
+ styleFromPropValue: (value: any) => any
+): any;
+
/**
* @returns An enhanced stylefunction that considers breakpoints
*/
diff --git a/packages/material-ui-system/src/index.js b/packages/material-ui-system/src/index.js
index 46dce6a29323d7..d1fe90e016ba03 100644
--- a/packages/material-ui-system/src/index.js
+++ b/packages/material-ui-system/src/index.js
@@ -1,6 +1,7 @@
export { default as borders } from './borders';
export * from './borders';
export { default as breakpoints } from './breakpoints';
+export { handleBreakpoints } from './breakpoints';
export { default as compose } from './compose';
export { default as css } from './css';
export { default as display } from './display';
diff --git a/packages/material-ui-system/src/palette.js b/packages/material-ui-system/src/palette.js
index 9bb9bb8b32c3d9..cd4586a57cfa34 100644
--- a/packages/material-ui-system/src/palette.js
+++ b/packages/material-ui-system/src/palette.js
@@ -12,6 +12,11 @@ export const bgcolor = style({
themeKey: 'palette',
});
-const palette = compose(color, bgcolor);
+export const backgroundColor = style({
+ prop: 'backgroundColor',
+ themeKey: 'palette',
+});
+
+const palette = compose(color, bgcolor, backgroundColor);
export default palette;
diff --git a/packages/material-ui/src/Box/Box.d.ts b/packages/material-ui/src/Box/Box.d.ts
index 7b29a250ea3a02..675f5fee90be3a 100644
--- a/packages/material-ui/src/Box/Box.d.ts
+++ b/packages/material-ui/src/Box/Box.d.ts
@@ -13,9 +13,11 @@ import {
typography,
PropsFor,
} from '@material-ui/system';
+import { Theme } from '../styles/createMuiTheme';
+import { CSSObject } from '../styles/experimentalStyled';
import { Omit } from '..';
-type BoxStyleFunction = ComposedStyleFunction<
+export type BoxStyleFunction = ComposedStyleFunction<
[
typeof borders,
typeof display,
@@ -32,14 +34,23 @@ type BoxStyleFunction = ComposedStyleFunction<
type SystemProps = PropsFor;
type ElementProps = Omit, keyof SystemProps>;
+type SxPropsValue = Omit & SystemProps;
+type SxProps = {
+ [Name in keyof SxPropsValue]?:
+ | SxPropsValue[Name]
+ | ((theme: Theme) => CSSObject | SxPropsValue[Name])
+ | SxProps;
+};
export interface BoxProps extends ElementProps, SystemProps {
+ children?: React.ReactNode | ((props: ElementProps) => React.ReactNode);
// styled API
component?: React.ElementType;
clone?: boolean;
ref?: React.Ref;
// workaround for https://github.com/mui-org/material-ui/pull/15611
css?: SystemProps;
+ sx?: SxProps;
}
declare const Box: React.ComponentType;
diff --git a/packages/material-ui/src/Box/Box.js b/packages/material-ui/src/Box/Box.js
index a01f4dac7f8849..998699a8de06e2 100644
--- a/packages/material-ui/src/Box/Box.js
+++ b/packages/material-ui/src/Box/Box.js
@@ -1,37 +1,9 @@
import * as React from 'react';
import PropTypes from 'prop-types';
-import {
- borders,
- compose,
- display,
- flexbox,
- grid,
- palette,
- positions,
- shadows,
- sizing,
- spacing,
- typography,
- css,
-} from '@material-ui/system';
import clsx from 'clsx';
+import styleFunction from './styleFunction';
import styled from '../styles/experimentalStyled';
-export const styleFunction = css(
- compose(
- borders,
- display,
- flexbox,
- grid,
- positions,
- palette,
- shadows,
- sizing,
- spacing,
- typography,
- ),
-);
-
function omit(input, fields) {
const output = {};
@@ -71,7 +43,7 @@ const BoxRoot = React.forwardRef(function StyledComponent(props, ref) {
});
BoxRoot.propTypes = {
- children: PropTypes.node,
+ children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
className: PropTypes.string,
clone: PropTypes.bool,
component: PropTypes.elementType,
diff --git a/packages/material-ui/src/Box/index.d.ts b/packages/material-ui/src/Box/index.d.ts
index 38ce2fc4f25954..a6fe0c7c004d42 100644
--- a/packages/material-ui/src/Box/index.d.ts
+++ b/packages/material-ui/src/Box/index.d.ts
@@ -1,2 +1,4 @@
export { default } from './Box';
export * from './Box';
+export { default as styleFunction } from './styleFunction';
+export * from './styleFunction';
diff --git a/packages/material-ui/src/Box/index.js b/packages/material-ui/src/Box/index.js
index bdc699f271dc49..c15b792255b474 100644
--- a/packages/material-ui/src/Box/index.js
+++ b/packages/material-ui/src/Box/index.js
@@ -1 +1,2 @@
-export { default, styleFunction } from './Box';
+export { default } from './Box';
+export { default as styleFunction } from './styleFunction';
diff --git a/packages/material-ui/src/Box/styleFunction.d.ts b/packages/material-ui/src/Box/styleFunction.d.ts
new file mode 100644
index 00000000000000..3c201089140988
--- /dev/null
+++ b/packages/material-ui/src/Box/styleFunction.d.ts
@@ -0,0 +1,4 @@
+import { BoxStyleFunction } from './Box';
+
+declare const styleFunction: BoxStyleFunction;
+export default styleFunction;
diff --git a/packages/material-ui/src/Box/styleFunction.js b/packages/material-ui/src/Box/styleFunction.js
new file mode 100644
index 00000000000000..112ae750da00b0
--- /dev/null
+++ b/packages/material-ui/src/Box/styleFunction.js
@@ -0,0 +1,129 @@
+import {
+ borders,
+ display,
+ flexbox,
+ grid,
+ positions,
+ palette,
+ shadows,
+ sizing,
+ spacing,
+ typography,
+ handleBreakpoints,
+} from '@material-ui/system';
+import { deepmerge } from '@material-ui/utils';
+
+function objectsHaveSameKeys(...objects) {
+ const allKeys = objects.reduce((keys, object) => keys.concat(Object.keys(object)), []);
+ const union = new Set(allKeys);
+ return objects.every((object) => union.size === Object.keys(object).length);
+}
+
+const filterProps = [
+ ...borders.filterProps,
+ ...display.filterProps,
+ ...flexbox.filterProps,
+ ...grid.filterProps,
+ ...positions.filterProps,
+ ...palette.filterProps,
+ ...shadows.filterProps,
+ ...sizing.filterProps,
+ ...spacing.filterProps,
+ ...typography.filterProps,
+ 'sx',
+];
+
+const getThemeValue = (prop, value, theme) => {
+ const inputProps = {
+ [prop]: value,
+ theme,
+ };
+
+ if (borders.filterProps.indexOf(prop) !== -1) {
+ return borders(inputProps);
+ }
+ if (display.filterProps.indexOf(prop) !== -1) {
+ return display(inputProps);
+ }
+ if (flexbox.filterProps.indexOf(prop) !== -1) {
+ return flexbox(inputProps);
+ }
+ if (grid.filterProps.indexOf(prop) !== -1) {
+ return grid(inputProps);
+ }
+ if (positions.filterProps.indexOf(prop) !== -1) {
+ return positions(inputProps);
+ }
+ if (palette.filterProps.indexOf(prop) !== -1) {
+ return palette(inputProps);
+ }
+ if (shadows.filterProps.indexOf(prop) !== -1) {
+ return shadows(inputProps);
+ }
+ if (sizing.filterProps.indexOf(prop) !== -1) {
+ return sizing(inputProps);
+ }
+ if (spacing.filterProps.indexOf(prop) !== -1) {
+ return spacing(inputProps);
+ }
+ if (typography.filterProps.indexOf(prop) !== -1) {
+ return typography(inputProps);
+ }
+ return { [prop]: value };
+};
+
+const styleFunctionSx = (styles, theme) => {
+ if (!styles) return null;
+
+ if (typeof styles === 'function') {
+ return styles(theme);
+ }
+
+ if (typeof styles !== 'object') {
+ // value
+ return styles;
+ }
+
+ let css = {};
+
+ Object.keys(styles).forEach((styleKey) => {
+ if (typeof styles[styleKey] === 'object') {
+ if (filterProps.indexOf(styleKey) !== -1) {
+ css = deepmerge(css, getThemeValue(styleKey, styles[styleKey], theme));
+ } else {
+ const breakpointsValues = handleBreakpoints({ theme }, styles[styleKey], (x) => ({
+ [styleKey]: x,
+ }));
+
+ if (objectsHaveSameKeys(breakpointsValues, styles[styleKey])) {
+ const transformedValue = styleFunctionSx(styles[styleKey], theme);
+ css[styleKey] = transformedValue;
+ } else {
+ css = deepmerge(css, breakpointsValues);
+ }
+ }
+ } else if (typeof styles[styleKey] === 'function') {
+ css = deepmerge(css, { [styleKey]: styles[styleKey](theme) });
+ } else {
+ css = deepmerge(css, getThemeValue(styleKey, styles[styleKey], theme));
+ }
+ });
+ return css;
+};
+
+const styleFunction = (props) => {
+ let result = {};
+ Object.keys(props).forEach((prop) => {
+ if (filterProps.indexOf(prop) !== -1 && prop !== 'sx') {
+ result = deepmerge(result, getThemeValue(prop, props[prop], props.theme));
+ }
+ });
+
+ const sxValue = styleFunctionSx(props.sx, props.theme);
+
+ return deepmerge(result, sxValue);
+};
+
+styleFunction.filterProps = filterProps;
+
+export default styleFunction;
diff --git a/packages/material-ui/src/Box/styleFunction.test.js b/packages/material-ui/src/Box/styleFunction.test.js
new file mode 100644
index 00000000000000..45d281a8fb7cb1
--- /dev/null
+++ b/packages/material-ui/src/Box/styleFunction.test.js
@@ -0,0 +1,217 @@
+import { expect } from 'chai';
+import { createMuiTheme } from '@material-ui/core/styles';
+import styleFunction from './styleFunction';
+
+describe('styleFunction', () => {
+ const theme = createMuiTheme({
+ spacing: 10,
+ palette: {
+ primary: {
+ main: 'rgb(0, 0, 255)',
+ },
+ secondary: {
+ main: 'rgb(0, 255, 0)',
+ },
+ },
+ });
+
+ it('resolves palette', () => {
+ const result = styleFunction({
+ theme,
+ color: 'primary.main',
+ bgcolor: 'secondary.main',
+ });
+
+ expect(result).to.deep.equal({
+ color: 'rgb(0, 0, 255)',
+ backgroundColor: 'rgb(0, 255, 0)',
+ });
+ });
+
+ it('resolves spacing', () => {
+ const result = styleFunction({
+ theme,
+ m: 2,
+ p: 1,
+ });
+
+ expect(result).to.deep.equal({
+ margin: '20px',
+ padding: '10px',
+ });
+ });
+
+ it('resolves typography', () => {
+ const result = styleFunction({
+ theme,
+ fontFamily: 'fontFamily',
+ fontWeight: 'fontWeightLight',
+ fontSize: 'fontSize',
+ });
+
+ expect(result).to.deep.equal({
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
+ fontWeight: 300,
+ fontSize: 14,
+ });
+ });
+
+ it('resolves display', () => {
+ const result = styleFunction({
+ theme,
+ displayPrint: 'block',
+ });
+
+ expect(result).to.deep.equal({
+ '@media print': {
+ display: 'block',
+ },
+ });
+ });
+
+ it('resolves borders', () => {
+ const result = styleFunction({
+ theme,
+ border: 1,
+ borderColor: 'black',
+ });
+
+ expect(result).to.deep.equal({
+ border: '1px solid',
+ borderColor: 'black',
+ });
+ });
+
+ describe('sx prop', () => {
+ it('resolves system', () => {
+ const result = styleFunction({
+ theme,
+ sx: {
+ color: 'primary.main',
+ bgcolor: 'secondary.main',
+ m: 2,
+ p: 1,
+ fontFamily: 'fontFamily',
+ fontWeight: 'fontWeightLight',
+ fontSize: 'fontSize',
+ displayPrint: 'block',
+ border: [1, 2, 3, 4, 5],
+ },
+ });
+
+ expect(result).to.deep.equal({
+ color: 'rgb(0, 0, 255)',
+ backgroundColor: 'rgb(0, 255, 0)',
+ margin: '20px',
+ padding: '10px',
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
+ fontWeight: 300,
+ fontSize: 14,
+ '@media print': {
+ display: 'block',
+ },
+ '@media (min-width:0px)': { border: '1px solid' },
+ '@media (min-width:600px)': { border: '2px solid' },
+ '@media (min-width:960px)': { border: '3px solid' },
+ '@media (min-width:1280px)': { border: '4px solid' },
+ '@media (min-width:1920px)': { border: '5px solid' },
+ });
+ });
+
+ it('resolves non system CSS properties if specified', () => {
+ const result = styleFunction({
+ theme,
+ sx: {
+ background: 'rgb(0, 0, 255)',
+ ':hover': {
+ backgroundColor: 'primary.main',
+ opacity: {
+ xs: 0.1,
+ sm: 0.2,
+ md: 0.3,
+ lg: 0.4,
+ xl: 0.5,
+ },
+ border: [1, 2, 3],
+ borderColor: (t) => t.palette.secondary.main,
+ },
+ },
+ });
+
+ expect(result).to.deep.equal({
+ background: 'rgb(0, 0, 255)',
+ ':hover': {
+ backgroundColor: 'rgb(0, 0, 255)',
+ '@media (min-width:0px)': { opacity: 0.1, border: '1px solid' },
+ '@media (min-width:600px)': { opacity: 0.2, border: '2px solid' },
+ '@media (min-width:960px)': { opacity: 0.3, border: '3px solid' },
+ '@media (min-width:1280px)': { opacity: 0.4 },
+ '@media (min-width:1920px)': { opacity: 0.5 },
+ borderColor: 'rgb(0, 255, 0)',
+ },
+ });
+ });
+ });
+
+ describe('breakpoints', () => {
+ const breakpointsExpectedResult = {
+ '@media (min-width:0px)': { border: '1px solid' },
+ '@media (min-width:600px)': { border: '2px solid' },
+ '@media (min-width:960px)': { border: '3px solid' },
+ '@media (min-width:1280px)': { border: '4px solid' },
+ '@media (min-width:1920px)': { border: '5px solid' },
+ };
+
+ it('resolves breakpoints array', () => {
+ const result = styleFunction({
+ theme,
+ border: [1, 2, 3, 4, 5],
+ });
+
+ expect(result).to.deep.equal(breakpointsExpectedResult);
+ });
+
+ it('resolves breakpoints object', () => {
+ const result = styleFunction({
+ theme,
+ border: {
+ xs: 1,
+ sm: 2,
+ md: 3,
+ lg: 4,
+ xl: 5,
+ },
+ });
+
+ expect(result).to.deep.equal(breakpointsExpectedResult);
+ });
+
+ it('merges multiple breakpoints object', () => {
+ const result = styleFunction({
+ theme,
+ m: [1, 2, 3],
+ p: [5, 6, 7],
+ });
+
+ expect(result).to.deep.equal({
+ '@media (min-width:0px)': { padding: '50px', margin: '10px' },
+ '@media (min-width:600px)': { padding: '60px', margin: '20px' },
+ '@media (min-width:960px)': { padding: '70px', margin: '30px' },
+ });
+ });
+
+ it('merges breakpoints from props and sx', () => {
+ const result = styleFunction({
+ theme,
+ m: [1, 2, 3],
+ sx: { p: [5, 6, 7] },
+ });
+
+ expect(result).to.deep.equal({
+ '@media (min-width:0px)': { padding: '50px', margin: '10px' },
+ '@media (min-width:600px)': { padding: '60px', margin: '20px' },
+ '@media (min-width:960px)': { padding: '70px', margin: '30px' },
+ });
+ });
+ });
+});