diff --git a/docs/data/material/customization/css-layers/CssLayersCaveat.js b/docs/data/material/customization/css-layers/CssLayersCaveat.js
new file mode 100644
index 00000000000000..5ea05464d76d8d
--- /dev/null
+++ b/docs/data/material/customization/css-layers/CssLayersCaveat.js
@@ -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 (
+
+
+
+ No CSS Layers
+
+ setCssLayers(!cssLayers)} />
+
+ With CSS Layers
+
+
+
+
+
+ }
+ aria-controls="panel1-content"
+ id="panel1-header"
+ >
+ Accordion 1
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
+ malesuada lacus ex, sit amet blandit leo lobortis eget.
+
+
+
+ }
+ aria-controls="panel2-content"
+ id="panel2-header"
+ >
+ Accordion 2
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
+ malesuada lacus ex, sit amet blandit leo lobortis eget.
+
+
+
+
+
+ );
+}
diff --git a/docs/data/material/customization/css-layers/CssLayersCaveat.tsx b/docs/data/material/customization/css-layers/CssLayersCaveat.tsx
new file mode 100644
index 00000000000000..5ea05464d76d8d
--- /dev/null
+++ b/docs/data/material/customization/css-layers/CssLayersCaveat.tsx
@@ -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 (
+
+
+
+ No CSS Layers
+
+ setCssLayers(!cssLayers)} />
+
+ With CSS Layers
+
+
+
+
+
+ }
+ aria-controls="panel1-content"
+ id="panel1-header"
+ >
+ Accordion 1
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
+ malesuada lacus ex, sit amet blandit leo lobortis eget.
+
+
+
+ }
+ aria-controls="panel2-content"
+ id="panel2-header"
+ >
+ Accordion 2
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
+ malesuada lacus ex, sit amet blandit leo lobortis eget.
+
+
+
+
+
+ );
+}
diff --git a/docs/data/material/customization/css-layers/CssLayersInput.js b/docs/data/material/customization/css-layers/CssLayersInput.js
new file mode 100644
index 00000000000000..67fe8323ce2ff1
--- /dev/null
+++ b/docs/data/material/customization/css-layers/CssLayersInput.js
@@ -0,0 +1,44 @@
+import * as React from 'react';
+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 (
+
+
+
+ Label
+
+
+ Helper text goes here
+
+
+ );
+}
diff --git a/docs/data/material/customization/css-layers/CssLayersInput.tsx b/docs/data/material/customization/css-layers/CssLayersInput.tsx
new file mode 100644
index 00000000000000..67fe8323ce2ff1
--- /dev/null
+++ b/docs/data/material/customization/css-layers/CssLayersInput.tsx
@@ -0,0 +1,44 @@
+import * as React from 'react';
+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 (
+
+
+
+ Label
+
+
+ Helper text goes here
+
+
+ );
+}
diff --git a/docs/data/material/customization/css-layers/css-layers.md b/docs/data/material/customization/css-layers/css-layers.md
new file mode 100644
index 00000000000000..f5e1813660c35f
--- /dev/null
+++ b/docs/data/material/customization/css-layers/css-layers.md
@@ -0,0 +1,287 @@
+# CSS Layers
+
+Learn how to generate Material UI styles with cascade layers.
+
+## 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 (
+
+
+
+ {/* Your app */}
+
+
+
+ );
+}
+```
+
+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 (
+
+
+
+
+ );
+}
+```
+
+### 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(
+
+
+
+ {/* Your app */}
+
+ ,
+);
+```
+
+## 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 {children};
+}
+```
+
+{{"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 {children};
+}
+```
+
+```tsx title="src/app/layout.tsx"
+import AppTheme from '../theme';
+
+export default function RootLayout() {
+ return (
+
+
+
+ {/* Your app */}
+
+
+
+ );
+}
+```
+
+### 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 {children};
+}
+```
+
+```tsx title="pages/_app.tsx"
+import AppTheme from '../src/theme';
+
+export default function MyApp(props: AppProps) {
+ const { Component, pageProps } = props;
+ return (
+
+
+
+
+
+ );
+}
+```
+
+```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 {children};
+}
+```
+
+```tsx title="src/main.tsx"
+import AppTheme from './theme';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ {/* Your app */}
+
+ ,
+);
+```
+
+### 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"}}
diff --git a/docs/data/material/integrations/nextjs/nextjs.md b/docs/data/material/integrations/nextjs/nextjs.md
index 631b25d54886ba..929bfd30a48fd4 100644
--- a/docs/data/material/integrations/nextjs/nextjs.md
+++ b/docs/data/material/integrations/nextjs/nextjs.md
@@ -118,7 +118,7 @@ Finally, in `src/app/layout.tsx`, pass the theme to the `ThemeProvider`:
To learn more about theming, check out the [theming guide](/material-ui/customization/theming/) page.
-#### CSS theme variables
+### CSS theme variables
To use [CSS theme variables](/material-ui/customization/css-theme-variables/overview/), enable the `cssVariables` flag:
@@ -243,6 +243,40 @@ To use a custom [Emotion cache](https://emotion.sh/docs/@emotion/cache), pass it
};
```
+#### Cascade layers (optional)
+
+To enable [cascade layers](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Cascade_layers) (`@layer`), create a new cache with `enableCssLayer: true` and pass it to the `emotionCache` property in both `_document.tsx` and `_app.tsx`:
+
+```diff title="pages/_document.tsx"
++import { createEmotionCache } from '@mui/material-nextjs/v15-pagesRouter';
+ ...
+
+ MyDocument.getInitialProps = async (ctx) => {
+ const finalProps = await documentGetInitialProps(ctx, {
++ emotionCache: createEmotionCache({ enableCssLayer: true }),
+ });
+ return finalProps;
+ };
+```
+
+```diff title="pages/_app.tsx"
++import { createEmotionCache } from '@mui/material-nextjs/v15-pagesRouter';
+ ...
+
+const clientCache = createEmotionCache({ enableCssLayer: true });
+
++ export default function MyApp({ emotionCache = clientCache }) {
+ return (
++
+
+ ...
+
+ ...
+
+ );
+ }
+```
+
#### App enhancement (optional)
Pass an array to the `plugins` property to enhance the app with additional features, like server-side-rendered styles if you're using JSS and styled-components.
diff --git a/docs/data/material/pages.ts b/docs/data/material/pages.ts
index c20ab622ef9eab..a3fa03e02d2eeb 100644
--- a/docs/data/material/pages.ts
+++ b/docs/data/material/pages.ts
@@ -232,6 +232,18 @@ const pages: MuiPage[] = [
},
],
},
+ {
+ pathname: '/material-ui/customization/styles',
+ subheader: '/material-ui/customization/styles',
+ title: 'Styles',
+ children: [
+ {
+ pathname: '/material-ui/customization/css-layers',
+ title: 'Cascade layers',
+ newFeature: true,
+ },
+ ],
+ },
],
},
{
diff --git a/docs/pages/material-ui/customization/css-layers.js b/docs/pages/material-ui/customization/css-layers.js
new file mode 100644
index 00000000000000..cfd6e622bf484d
--- /dev/null
+++ b/docs/pages/material-ui/customization/css-layers.js
@@ -0,0 +1,7 @@
+import * as React from 'react';
+import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs';
+import * as pageProps from 'docs/data/material/customization/css-layers/css-layers.md?muiMarkdown';
+
+export default function Page() {
+ return ;
+}
diff --git a/docs/translations/translations.json b/docs/translations/translations.json
index e58b9cf8929464..b5be4ee71f56ac 100644
--- a/docs/translations/translations.json
+++ b/docs/translations/translations.json
@@ -242,6 +242,8 @@
"/material-ui/customization/css-theme-variables/overview": "Overview",
"/material-ui/customization/css-theme-variables/usage": "Basic usage",
"/material-ui/customization/css-theme-variables/configuration": "Advanced configuration",
+ "/material-ui/customization/styles": "Styles",
+ "/material-ui/customization/css-layers": "Cascade layers",
"/material-ui/guides": "How-to guides",
"/material-ui/guides/minimizing-bundle-size": "Minimizing bundle size",
"/material-ui/guides/server-rendering": "Server rendering",
diff --git a/packages/mui-material-nextjs/src/v13-appRouter/appRouterV13.tsx b/packages/mui-material-nextjs/src/v13-appRouter/appRouterV13.tsx
index 3de6fb424a10a5..620026893b5428 100644
--- a/packages/mui-material-nextjs/src/v13-appRouter/appRouterV13.tsx
+++ b/packages/mui-material-nextjs/src/v13-appRouter/appRouterV13.tsx
@@ -38,7 +38,7 @@ export default function AppRouterCacheProvider(props: AppRouterCacheProviderProp
let inserted: { name: string; isGlobal: boolean }[] = [];
// Override the insert method to support streaming SSR with flush().
cache.insert = (...args) => {
- if (options?.enableCssLayer && !args[1].styles.startsWith('@layer')) {
+ if (options?.enableCssLayer && !args[1].styles.match(/^@layer\s+[^{]*$/)) {
args[1].styles = `@layer mui {${args[1].styles}}`;
}
const [selector, serialized] = args;
diff --git a/packages/mui-material-nextjs/src/v13-pagesRouter/createCache.ts b/packages/mui-material-nextjs/src/v13-pagesRouter/createCache.ts
index fce4601e4ca2bc..a459a31c5fe886 100644
--- a/packages/mui-material-nextjs/src/v13-pagesRouter/createCache.ts
+++ b/packages/mui-material-nextjs/src/v13-pagesRouter/createCache.ts
@@ -5,7 +5,9 @@ const isBrowser = typeof document !== 'undefined';
// On the client side, Create a meta tag at the top of the and set it as insertionPoint.
// This assures that MUI styles are loaded first.
// It allows developers to easily override MUI styles with other styling solutions, like CSS modules.
-export default function createEmotionCache() {
+export default function createEmotionCache(
+ options?: { enableCssLayer?: boolean } & Parameters[0],
+) {
let insertionPoint;
if (isBrowser) {
@@ -15,5 +17,18 @@ export default function createEmotionCache() {
insertionPoint = emotionInsertionPoint ?? undefined;
}
- return createCache({ key: 'mui', insertionPoint });
+ const { enableCssLayer, ...rest } = options ?? {};
+
+ const emotionCache = createCache({ key: 'mui', insertionPoint, ...rest });
+ if (enableCssLayer) {
+ const prevInsert = emotionCache.insert;
+ emotionCache.insert = (...args) => {
+ // ignore styles that contain layer order (`@layer ...` without `{`)
+ if (!args[1].styles.match(/^@layer\s+[^{]*$/)) {
+ args[1].styles = `@layer mui {${args[1].styles}}`;
+ }
+ return prevInsert(...args);
+ };
+ }
+ return emotionCache;
}
diff --git a/packages/mui-material-nextjs/src/v13-pagesRouter/index.ts b/packages/mui-material-nextjs/src/v13-pagesRouter/index.ts
index ff4f92e5707270..d4c47def5c341d 100644
--- a/packages/mui-material-nextjs/src/v13-pagesRouter/index.ts
+++ b/packages/mui-material-nextjs/src/v13-pagesRouter/index.ts
@@ -1,2 +1,3 @@
export * from './pagesRouterV13Document';
export * from './pagesRouterV13App';
+export { default as createEmotionCache } from './createCache';
diff --git a/packages/mui-material-nextjs/src/v13-pagesRouter/pagesRouterV13Document.tsx b/packages/mui-material-nextjs/src/v13-pagesRouter/pagesRouterV13Document.tsx
index 924b38638f4bfc..1594b57a624ade 100644
--- a/packages/mui-material-nextjs/src/v13-pagesRouter/pagesRouterV13Document.tsx
+++ b/packages/mui-material-nextjs/src/v13-pagesRouter/pagesRouterV13Document.tsx
@@ -99,14 +99,23 @@ export async function documentGetInitialProps(
const { styles } = extractCriticalToChunks(initialProps.html);
return {
...initialProps,
- emotionStyleTags: styles.map((style) => (
-
- )),
+ emotionStyleTags: styles.map((style) => {
+ if (!style.css.trim()) {
+ return null;
+ }
+ const isLayerOrderRule = style.css.startsWith('@layer') && !style.css.match(/\{.*\}/);
+ return (
+
+ );
+ }),
};
},
},
diff --git a/packages/mui-material/src/Breadcrumbs/BreadcrumbCollapsed.js b/packages/mui-material/src/Breadcrumbs/BreadcrumbCollapsed.js
index 5b4f936916a317..2243957ec69bc8 100644
--- a/packages/mui-material/src/Breadcrumbs/BreadcrumbCollapsed.js
+++ b/packages/mui-material/src/Breadcrumbs/BreadcrumbCollapsed.js
@@ -7,7 +7,9 @@ import memoTheme from '../utils/memoTheme';
import MoreHorizIcon from '../internal/svg-icons/MoreHoriz';
import ButtonBase from '../ButtonBase';
-const BreadcrumbCollapsedButton = styled(ButtonBase)(
+const BreadcrumbCollapsedButton = styled(ButtonBase, {
+ name: 'MuiBreadcrumbCollapsed',
+})(
memoTheme(({ theme }) => ({
display: 'flex',
marginLeft: `calc(${theme.spacing(1)} * 0.5)`,
diff --git a/packages/mui-material/src/NativeSelect/NativeSelectInput.js b/packages/mui-material/src/NativeSelect/NativeSelectInput.js
index 4de10704548665..68f1a1ac13a229 100644
--- a/packages/mui-material/src/NativeSelect/NativeSelectInput.js
+++ b/packages/mui-material/src/NativeSelect/NativeSelectInput.js
@@ -20,7 +20,9 @@ const useUtilityClasses = (ownerState) => {
return composeClasses(slots, getNativeSelectUtilityClasses, classes);
};
-export const StyledSelectSelect = styled('select')(({ theme }) => ({
+export const StyledSelectSelect = styled('select', {
+ name: 'MuiNativeSelect',
+})(({ theme }) => ({
// Reset
MozAppearance: 'none',
// Reset
@@ -99,7 +101,9 @@ const NativeSelectSelect = styled(StyledSelectSelect, {
},
})({});
-export const StyledSelectIcon = styled('svg')(({ theme }) => ({
+export const StyledSelectIcon = styled('svg', {
+ name: 'MuiNativeSelect',
+})(({ theme }) => ({
// We use a position absolute over a flexbox in order to forward the pointer events
// to the input and to support wrapping tags..
position: 'absolute',
diff --git a/packages/mui-material/src/OutlinedInput/NotchedOutline.js b/packages/mui-material/src/OutlinedInput/NotchedOutline.js
index 0fff01342a45ff..1dee672985751d 100644
--- a/packages/mui-material/src/OutlinedInput/NotchedOutline.js
+++ b/packages/mui-material/src/OutlinedInput/NotchedOutline.js
@@ -5,7 +5,10 @@ import rootShouldForwardProp from '../styles/rootShouldForwardProp';
import { styled } from '../zero-styled';
import memoTheme from '../utils/memoTheme';
-const NotchedOutlineRoot = styled('fieldset', { shouldForwardProp: rootShouldForwardProp })({
+const NotchedOutlineRoot = styled('fieldset', {
+ name: 'MuiNotchedOutlined',
+ shouldForwardProp: rootShouldForwardProp,
+})({
textAlign: 'left',
position: 'absolute',
bottom: 0,
@@ -22,7 +25,10 @@ const NotchedOutlineRoot = styled('fieldset', { shouldForwardProp: rootShouldFor
minWidth: '0%',
});
-const NotchedOutlineLegend = styled('legend', { shouldForwardProp: rootShouldForwardProp })(
+const NotchedOutlineLegend = styled('legend', {
+ name: 'MuiNotchedOutlined',
+ shouldForwardProp: rootShouldForwardProp,
+})(
memoTheme(({ theme }) => ({
float: 'unset', // Fix conflict with bootstrap
width: 'auto', // Fix conflict with bootstrap
diff --git a/packages/mui-material/src/Radio/RadioButtonIcon.js b/packages/mui-material/src/Radio/RadioButtonIcon.js
index ac9cb9892a4c3e..54e97ca7ba183e 100644
--- a/packages/mui-material/src/Radio/RadioButtonIcon.js
+++ b/packages/mui-material/src/Radio/RadioButtonIcon.js
@@ -7,17 +7,24 @@ import rootShouldForwardProp from '../styles/rootShouldForwardProp';
import { styled } from '../zero-styled';
import memoTheme from '../utils/memoTheme';
-const RadioButtonIconRoot = styled('span', { shouldForwardProp: rootShouldForwardProp })({
+const RadioButtonIconRoot = styled('span', {
+ name: 'MuiRadioButtonIcon',
+ shouldForwardProp: rootShouldForwardProp,
+})({
position: 'relative',
display: 'flex',
});
-const RadioButtonIconBackground = styled(RadioButtonUncheckedIcon)({
+const RadioButtonIconBackground = styled(RadioButtonUncheckedIcon, {
+ name: 'MuiRadioButtonIcon',
+})({
// Scale applied to prevent dot misalignment in Safari
transform: 'scale(1)',
});
-const RadioButtonIconDot = styled(RadioButtonCheckedIcon)(
+const RadioButtonIconDot = styled(RadioButtonCheckedIcon, {
+ name: 'MuiRadioButtonIcon',
+})(
memoTheme(({ theme }) => ({
left: 0,
position: 'absolute',
diff --git a/packages/mui-material/src/SwipeableDrawer/SwipeArea.js b/packages/mui-material/src/SwipeableDrawer/SwipeArea.js
index 69ca18085f2fcb..2f87634a2229d1 100644
--- a/packages/mui-material/src/SwipeableDrawer/SwipeArea.js
+++ b/packages/mui-material/src/SwipeableDrawer/SwipeArea.js
@@ -8,7 +8,10 @@ import rootShouldForwardProp from '../styles/rootShouldForwardProp';
import capitalize from '../utils/capitalize';
import { isHorizontal } from '../Drawer/Drawer';
-const SwipeAreaRoot = styled('div', { shouldForwardProp: rootShouldForwardProp })(
+const SwipeAreaRoot = styled('div', {
+ name: 'MuiSwipeArea',
+ shouldForwardProp: rootShouldForwardProp,
+})(
memoTheme(({ theme }) => ({
position: 'fixed',
top: 0,
diff --git a/packages/mui-material/src/internal/SwitchBase.js b/packages/mui-material/src/internal/SwitchBase.js
index d9468ded76f850..e659aa930adce4 100644
--- a/packages/mui-material/src/internal/SwitchBase.js
+++ b/packages/mui-material/src/internal/SwitchBase.js
@@ -23,7 +23,9 @@ const useUtilityClasses = (ownerState) => {
return composeClasses(slots, getSwitchBaseUtilityClass, classes);
};
-const SwitchBaseRoot = styled(ButtonBase)({
+const SwitchBaseRoot = styled(ButtonBase, {
+ name: 'MuiSwitchBase',
+})({
padding: 9,
borderRadius: '50%',
variants: [
@@ -60,7 +62,10 @@ const SwitchBaseRoot = styled(ButtonBase)({
],
});
-const SwitchBaseInput = styled('input', { shouldForwardProp: rootShouldForwardProp })({
+const SwitchBaseInput = styled('input', {
+ name: 'MuiSwitchBase',
+ shouldForwardProp: rootShouldForwardProp,
+})({
cursor: 'inherit',
position: 'absolute',
opacity: 0,
diff --git a/packages/mui-material/src/styles/createThemeNoVars.d.ts b/packages/mui-material/src/styles/createThemeNoVars.d.ts
index bc3c41911d676f..5740fc534d6f72 100644
--- a/packages/mui-material/src/styles/createThemeNoVars.d.ts
+++ b/packages/mui-material/src/styles/createThemeNoVars.d.ts
@@ -42,6 +42,7 @@ export interface ThemeOptions extends Omit, CssVar
zIndex?: ZIndexOptions;
unstable_strictMode?: boolean;
unstable_sxConfig?: SxConfig;
+ modularCssLayers?: boolean | string;
}
export interface BaseTheme extends SystemTheme {
diff --git a/packages/mui-material/src/styles/extendTheme.test.js b/packages/mui-material/src/styles/extendTheme.test.js
index 6bffb73787a98f..a6b334bf9bf167 100644
--- a/packages/mui-material/src/styles/extendTheme.test.js
+++ b/packages/mui-material/src/styles/extendTheme.test.js
@@ -865,4 +865,11 @@ describe('extendTheme', () => {
]);
});
});
+
+ it('should not generate vars for modularCssLayers', () => {
+ const theme = extendTheme({
+ modularCssLayers: '@layer mui,utilities;',
+ });
+ expect(theme.vars.modularCssLayers).to.equal(undefined);
+ });
});
diff --git a/packages/mui-material/src/styles/shouldSkipGeneratingVar.ts b/packages/mui-material/src/styles/shouldSkipGeneratingVar.ts
index 8b69b98cda490a..fe48bf4f40d0a3 100644
--- a/packages/mui-material/src/styles/shouldSkipGeneratingVar.ts
+++ b/packages/mui-material/src/styles/shouldSkipGeneratingVar.ts
@@ -1,7 +1,7 @@
export default function shouldSkipGeneratingVar(keys: string[]) {
return (
!!keys[0].match(
- /(cssVarPrefix|colorSchemeSelector|rootSelector|typography|mixins|breakpoints|direction|transitions)/,
+ /(cssVarPrefix|colorSchemeSelector|modularCssLayers|rootSelector|typography|mixins|breakpoints|direction|transitions)/,
) ||
!!keys[0].match(/sxConfig$/) || // ends with sxConfig
(keys[0] === 'palette' && !!keys[1]?.match(/(mode|contrastThreshold|tonalOffset)/))
diff --git a/packages/mui-styled-engine/src/StyledEngineProvider/StyledEngineProvider.js b/packages/mui-styled-engine/src/StyledEngineProvider/StyledEngineProvider.js
index 97986a009b9bea..4a2af3ef79a953 100644
--- a/packages/mui-styled-engine/src/StyledEngineProvider/StyledEngineProvider.js
+++ b/packages/mui-styled-engine/src/StyledEngineProvider/StyledEngineProvider.js
@@ -87,7 +87,7 @@ function getCache(injectFirst, enableCssLayer) {
if (enableCssLayer) {
const prevInsert = emotionCache.insert;
emotionCache.insert = (...args) => {
- if (!args[1].styles.startsWith('@layer')) {
+ if (!args[1].styles.match(/^@layer\s+[^{]*$/)) {
// avoid nested @layer
args[1].styles = `@layer mui {${args[1].styles}}`;
}
@@ -103,7 +103,7 @@ export default function StyledEngineProvider(props) {
const { injectFirst, enableCssLayer, children } = props;
const cache = React.useMemo(() => {
const cacheKey = `${injectFirst}-${enableCssLayer}`;
- if (cacheMap.has(cacheKey)) {
+ if (typeof document === 'object' && cacheMap.has(cacheKey)) {
return cacheMap.get(cacheKey);
}
const fresh = getCache(injectFirst, enableCssLayer);
diff --git a/packages/mui-styled-engine/src/StyledEngineProvider/StyledEngineProvider.test.js b/packages/mui-styled-engine/src/StyledEngineProvider/StyledEngineProvider.test.js
index 1ca028218a445e..cf8978e48e0bc3 100644
--- a/packages/mui-styled-engine/src/StyledEngineProvider/StyledEngineProvider.test.js
+++ b/packages/mui-styled-engine/src/StyledEngineProvider/StyledEngineProvider.test.js
@@ -33,13 +33,22 @@ describe('[Emotion] StyledEngineProvider', () => {
expect(rule).to.equal('@layer mui{html{color:red;}}');
});
- it('should do nothing if the styles already in a layer', () => {
+ it('should not do anything if the style is layer order', () => {
+ render(
+
+
+ ,
+ );
+ expect(rule).to.equal('@layer theme,base,mui,components,utilities;');
+ });
+
+ it('should wrap @layer rule', () => {
render(
,
);
- expect(rule).to.equal('@layer components{html{color:red;}}');
+ expect(rule).to.equal('@layer mui{@layer components{html{color:red;}}}');
});
it('able to config layer order through GlobalStyles', () => {
diff --git a/packages/mui-system/src/GlobalStyles/GlobalStyles.tsx b/packages/mui-system/src/GlobalStyles/GlobalStyles.tsx
index a1a26efeb762f8..4db4ba307742c8 100644
--- a/packages/mui-system/src/GlobalStyles/GlobalStyles.tsx
+++ b/packages/mui-system/src/GlobalStyles/GlobalStyles.tsx
@@ -1,7 +1,11 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
-import { GlobalStyles as MuiGlobalStyles, Interpolation } from '@mui/styled-engine';
+import {
+ GlobalStyles as MuiGlobalStyles,
+ Interpolation,
+ internal_serializeStyles as serializeStyles,
+} from '@mui/styled-engine';
import useTheme from '../useTheme';
import { Theme as SystemTheme } from '../createTheme';
@@ -11,17 +15,39 @@ export interface GlobalStylesProps {
themeId?: string;
}
+function wrapGlobalLayer(styles: any) {
+ const serialized = serializeStyles(styles) as { styles?: string };
+ if (styles !== serialized && serialized.styles) {
+ if (!serialized.styles.match(/^@layer\s+[^{]*$/)) {
+ // If the styles are not already wrapped in a layer, wrap them in a global layer.
+ serialized.styles = `@layer global{${serialized.styles}}`;
+ }
+ return serialized;
+ }
+ return styles;
+}
+
function GlobalStyles({
styles,
themeId,
defaultTheme = {},
}: GlobalStylesProps) {
const upperTheme = useTheme(defaultTheme);
+ const resolvedTheme = themeId ? (upperTheme as any)[themeId] || upperTheme : upperTheme;
- const globalStyles =
- typeof styles === 'function'
- ? styles(themeId ? (upperTheme as any)[themeId] || upperTheme : upperTheme)
- : styles;
+ let globalStyles = typeof styles === 'function' ? styles(resolvedTheme) : styles;
+ if (resolvedTheme.modularCssLayers) {
+ if (Array.isArray(globalStyles)) {
+ globalStyles = globalStyles.map((styleArg) => {
+ if (typeof styleArg === 'function') {
+ return wrapGlobalLayer(styleArg(resolvedTheme));
+ }
+ return wrapGlobalLayer(styleArg);
+ });
+ } else {
+ globalStyles = wrapGlobalLayer(globalStyles);
+ }
+ }
return ;
}
diff --git a/packages/mui-system/src/ThemeProvider/ThemeProvider.js b/packages/mui-system/src/ThemeProvider/ThemeProvider.js
index 7bfe2749b05f40..b64fe057ed98bf 100644
--- a/packages/mui-system/src/ThemeProvider/ThemeProvider.js
+++ b/packages/mui-system/src/ThemeProvider/ThemeProvider.js
@@ -10,6 +10,7 @@ import { ThemeContext as StyledEngineThemeContext } from '@mui/styled-engine';
import useThemeWithoutDefault from '../useThemeWithoutDefault';
import RtlProvider from '../RtlProvider';
import DefaultPropsProvider from '../DefaultPropsProvider';
+import useLayerOrder from './useLayerOrder';
const EMPTY_THEME = {};
@@ -64,6 +65,9 @@ function ThemeProvider(props) {
const engineTheme = useThemeScoping(themeId, upperTheme, localTheme);
const privateTheme = useThemeScoping(themeId, upperPrivateTheme, localTheme, true);
const rtlValue = (themeId ? engineTheme[themeId] : engineTheme).direction === 'rtl';
+
+ const layerOrder = useLayerOrder(engineTheme);
+
return (
@@ -71,6 +75,7 @@ function ThemeProvider(props) {
+ {layerOrder}
{children}
diff --git a/packages/mui-system/src/ThemeProvider/useLayerOrder.test.tsx b/packages/mui-system/src/ThemeProvider/useLayerOrder.test.tsx
new file mode 100644
index 00000000000000..ac2b759a8a1d86
--- /dev/null
+++ b/packages/mui-system/src/ThemeProvider/useLayerOrder.test.tsx
@@ -0,0 +1,58 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import { ThemeContext } from '@mui/styled-engine';
+import { createRenderer } from '@mui/internal-test-utils';
+import useLayerOrder from './useLayerOrder';
+
+function TestComponent({ theme }: { theme: any }) {
+ const LayerOrder = useLayerOrder(theme);
+ return LayerOrder;
+}
+
+describe('useLayerOrder', () => {
+ const { render } = createRenderer();
+
+ afterEach(() => {
+ // Clean up any injected style tags
+ document.querySelectorAll('style[data-mui-layer-order]').forEach((el) => el.remove());
+ });
+
+ it('attach layer order', () => {
+ const theme = { modularCssLayers: true };
+ render();
+ expect(document.head.firstChild).not.to.equal(null);
+ expect(document.head.firstChild?.textContent).to.contain(
+ '@layer mui.global, mui.components, mui.theme, mui.custom, mui.sx;',
+ );
+ });
+
+ it('custom layer order string', () => {
+ const theme = { modularCssLayers: '@layer theme, base, mui, utilities;' };
+ render();
+ expect(document.head.firstChild?.textContent).to.contain(
+ '@layer theme, base, mui.global, mui.components, mui.theme, mui.custom, mui.sx, utilities;',
+ );
+ });
+
+ it('does not replace nested layer', () => {
+ const theme = { modularCssLayers: '@layer theme, base, mui.unknown, utilities;' };
+ render();
+ expect(document.head.firstChild?.textContent).to.contain(
+ '@layer theme, base, mui.unknown, utilities;',
+ );
+ });
+
+ it('returns null if modularCssLayers is falsy', () => {
+ render();
+ expect(document.head.firstChild?.nodeName).not.to.equal('STYLE');
+ });
+
+ it('do nothing if upperTheme exists to avoid duplicate elements', () => {
+ render(
+
+
+ ,
+ );
+ expect(document.head.firstChild?.nodeName).not.to.equal('STYLE');
+ });
+});
diff --git a/packages/mui-system/src/ThemeProvider/useLayerOrder.tsx b/packages/mui-system/src/ThemeProvider/useLayerOrder.tsx
new file mode 100644
index 00000000000000..956ab4315da764
--- /dev/null
+++ b/packages/mui-system/src/ThemeProvider/useLayerOrder.tsx
@@ -0,0 +1,58 @@
+import * as React from 'react';
+import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
+import useId from '@mui/utils/useId';
+import GlobalStyles from '../GlobalStyles';
+import useThemeWithoutDefault from '../useThemeWithoutDefault';
+
+/**
+ * This hook returns a `GlobalStyles` component that sets the CSS layer order (for server-side rendering).
+ * Then on client-side, it injects the CSS layer order into the document head to ensure that the layer order is always present first before other Emotion styles.
+ */
+export default function useLayerOrder(theme: { modularCssLayers?: boolean | string }) {
+ const upperTheme = useThemeWithoutDefault();
+ const id = useId() || '';
+ const { modularCssLayers } = theme;
+
+ let layerOrder = 'mui.global, mui.components, mui.theme, mui.custom, mui.sx';
+
+ if (!modularCssLayers || upperTheme !== null) {
+ // skip this hook if upper theme exists.
+ layerOrder = '';
+ } else if (typeof modularCssLayers === 'string') {
+ layerOrder = modularCssLayers.replace(/mui(?!\.)/g, layerOrder);
+ } else {
+ layerOrder = `@layer ${layerOrder};`;
+ }
+
+ useEnhancedEffect(() => {
+ const head = document.querySelector('head');
+ if (!head) {
+ return;
+ }
+ const firstChild = head.firstChild as HTMLElement | null;
+
+ if (layerOrder) {
+ // Only insert if first child doesn't have data-mui-layer-order attribute
+ if (
+ firstChild &&
+ firstChild.hasAttribute?.('data-mui-layer-order') &&
+ firstChild.getAttribute('data-mui-layer-order') === id
+ ) {
+ return;
+ }
+ const styleElement = document.createElement('style');
+ styleElement.setAttribute('data-mui-layer-order', id);
+ styleElement.textContent = layerOrder;
+
+ head.prepend(styleElement);
+ } else {
+ head.querySelector(`style[data-mui-layer-order="${id}"]`)?.remove();
+ }
+ }, [layerOrder, id]);
+
+ if (!layerOrder) {
+ return null;
+ }
+
+ return ;
+}
diff --git a/packages/mui-system/src/createStyled/createStyled.js b/packages/mui-system/src/createStyled/createStyled.js
index 645d74399b0bff..04d3e63ac8d3e5 100644
--- a/packages/mui-system/src/createStyled/createStyled.js
+++ b/packages/mui-system/src/createStyled/createStyled.js
@@ -1,4 +1,7 @@
-import styledEngineStyled, { internal_mutateStyles as mutateStyles } from '@mui/styled-engine';
+import styledEngineStyled, {
+ internal_mutateStyles as mutateStyles,
+ internal_serializeStyles as serializeStyles,
+} from '@mui/styled-engine';
import { isPlainObject } from '@mui/utils/deepmerge';
import capitalize from '@mui/utils/capitalize';
import getDisplayName from '@mui/utils/getDisplayName';
@@ -17,6 +20,19 @@ export function shouldForwardProp(prop) {
return prop !== 'ownerState' && prop !== 'theme' && prop !== 'sx' && prop !== 'as';
}
+function shallowLayer(serialized, layerName) {
+ if (
+ layerName &&
+ serialized &&
+ typeof serialized === 'object' &&
+ serialized.styles &&
+ !serialized.styles.startsWith('@layer') // only add the layer if it is not already there.
+ ) {
+ serialized.styles = `@layer ${layerName}{${String(serialized.styles)}}`;
+ }
+ return serialized;
+}
+
function defaultOverridesResolver(slot) {
if (!slot) {
return null;
@@ -28,7 +44,7 @@ function attachTheme(props, themeId, defaultTheme) {
props.theme = isObjectEmpty(props.theme) ? defaultTheme : props.theme[themeId] || props.theme;
}
-function processStyle(props, style) {
+function processStyle(props, style, layerName) {
/*
* Style types:
* - null/undefined
@@ -41,29 +57,31 @@ function processStyle(props, style) {
const resolvedStyle = typeof style === 'function' ? style(props) : style;
if (Array.isArray(resolvedStyle)) {
- return resolvedStyle.flatMap((subStyle) => processStyle(props, subStyle));
+ return resolvedStyle.flatMap((subStyle) => processStyle(props, subStyle, layerName));
}
if (Array.isArray(resolvedStyle?.variants)) {
let rootStyle;
if (resolvedStyle.isProcessed) {
- rootStyle = resolvedStyle.style;
+ rootStyle = layerName ? shallowLayer(resolvedStyle.style, layerName) : resolvedStyle.style;
} else {
const { variants, ...otherStyles } = resolvedStyle;
- rootStyle = otherStyles;
+ rootStyle = layerName ? shallowLayer(serializeStyles(otherStyles), layerName) : otherStyles;
}
- return processStyleVariants(props, resolvedStyle.variants, [rootStyle]);
+ return processStyleVariants(props, resolvedStyle.variants, [rootStyle], layerName);
}
if (resolvedStyle?.isProcessed) {
- return resolvedStyle.style;
+ return layerName
+ ? shallowLayer(serializeStyles(resolvedStyle.style), layerName)
+ : resolvedStyle.style;
}
- return resolvedStyle;
+ return layerName ? shallowLayer(serializeStyles(resolvedStyle), layerName) : resolvedStyle;
}
-function processStyleVariants(props, variants, results = []) {
+function processStyleVariants(props, variants, results = [], layerName = undefined) {
let mergedState; // We might not need it, initialized lazily
variantLoop: for (let i = 0; i < variants.length; i += 1) {
@@ -84,9 +102,15 @@ function processStyleVariants(props, variants, results = []) {
if (typeof variant.style === 'function') {
mergedState ??= { ...props, ...props.ownerState, ownerState: props.ownerState };
- results.push(variant.style(mergedState));
+ results.push(
+ layerName
+ ? shallowLayer(serializeStyles(variant.style(mergedState)), layerName)
+ : variant.style(mergedState),
+ );
} else {
- results.push(variant.style);
+ results.push(
+ layerName ? shallowLayer(serializeStyles(variant.style), layerName) : variant.style,
+ );
}
}
@@ -121,6 +145,11 @@ export default function createStyled(input = {}) {
...options
} = inputOptions;
+ const layerName =
+ (componentName && componentName.startsWith('Mui')) || !!componentSlot
+ ? 'components'
+ : 'custom';
+
// if skipVariantsResolver option is defined, take the value, otherwise, true for root and false for other slots.
const skipVariantsResolver =
inputSkipVariantsResolver !== undefined
@@ -162,16 +191,22 @@ export default function createStyled(input = {}) {
}
if (typeof style === 'function') {
return function styleFunctionProcessor(props) {
- return processStyle(props, style);
+ return processStyle(props, style, props.theme.modularCssLayers ? layerName : undefined);
};
}
if (isPlainObject(style)) {
const serialized = preprocessStyles(style);
- if (!serialized.variants) {
- return serialized.style;
- }
return function styleObjectProcessor(props) {
- return processStyle(props, serialized);
+ if (!serialized.variants) {
+ return props.theme.modularCssLayers
+ ? shallowLayer(serialized.style, layerName)
+ : serialized.style;
+ }
+ return processStyle(
+ props,
+ serialized,
+ props.theme.modularCssLayers ? layerName : undefined,
+ );
};
}
return style;
@@ -199,7 +234,11 @@ export default function createStyled(input = {}) {
// TODO: v7 remove iteration and use `resolveStyleArg(styleOverrides[slot])` directly
// eslint-disable-next-line guard-for-in
for (const slotKey in styleOverrides) {
- resolvedStyleOverrides[slotKey] = processStyle(props, styleOverrides[slotKey]);
+ resolvedStyleOverrides[slotKey] = processStyle(
+ props,
+ styleOverrides[slotKey],
+ props.theme.modularCssLayers ? 'theme' : undefined,
+ );
}
return overridesResolver(props, resolvedStyleOverrides);
@@ -213,7 +252,12 @@ export default function createStyled(input = {}) {
if (!themeVariants) {
return null;
}
- return processStyleVariants(props, themeVariants);
+ return processStyleVariants(
+ props,
+ themeVariants,
+ [],
+ props.theme.modularCssLayers ? 'theme' : undefined,
+ );
});
}
diff --git a/packages/mui-system/src/styleFunctionSx/styleFunctionSx.js b/packages/mui-system/src/styleFunctionSx/styleFunctionSx.js
index a74b44b553311c..6c7d0e4391b483 100644
--- a/packages/mui-system/src/styleFunctionSx/styleFunctionSx.js
+++ b/packages/mui-system/src/styleFunctionSx/styleFunctionSx.js
@@ -76,7 +76,7 @@ export function unstable_createStyleFunctionSx() {
}
function styleFunctionSx(props) {
- const { sx, theme = {} } = props || {};
+ const { sx, theme = {}, nested } = props || {};
if (!sx) {
return null; // Emotion & styled-components will neglect null
@@ -117,7 +117,7 @@ export function unstable_createStyleFunctionSx() {
}));
if (objectsHaveSameKeys(breakpointsValues, value)) {
- css[styleKey] = styleFunctionSx({ sx: value, theme });
+ css[styleKey] = styleFunctionSx({ sx: value, theme, nested: true });
} else {
css = merge(css, breakpointsValues);
}
@@ -128,6 +128,12 @@ export function unstable_createStyleFunctionSx() {
}
});
+ if (!nested && theme.modularCssLayers) {
+ return {
+ '@layer sx': sortContainerQueries(theme, removeUnusedBreakpoints(breakpointsKeys, css)),
+ };
+ }
+
return sortContainerQueries(theme, removeUnusedBreakpoints(breakpointsKeys, css));
}
diff --git a/packages/mui-system/src/styleFunctionSx/styleFunctionSx.test.js b/packages/mui-system/src/styleFunctionSx/styleFunctionSx.test.js
index d8ca1d7a8e778e..f4cac85e4ae2d7 100644
--- a/packages/mui-system/src/styleFunctionSx/styleFunctionSx.test.js
+++ b/packages/mui-system/src/styleFunctionSx/styleFunctionSx.test.js
@@ -477,4 +477,140 @@ describe('styleFunctionSx', () => {
).not.to.throw();
});
});
+
+ describe('Modular CSS layers', () => {
+ it('should wrapped in @layer', () => {
+ const result = styleFunctionSx({
+ theme: {
+ ...theme,
+ modularCssLayers: true,
+ },
+ sx: {
+ color: 'primary.main',
+ bgcolor: 'secondary.main',
+ outline: 1,
+ outlineColor: 'secondary.main',
+ m: 2,
+ p: 1,
+ fontFamily: 'default',
+ fontWeight: 'light',
+ fontSize: 'fontSize',
+ maxWidth: 'sm',
+ },
+ });
+
+ expect(result).to.deep.equal({
+ '@layer sx': {
+ color: 'rgb(0, 0, 255)',
+ backgroundColor: 'rgb(0, 255, 0)',
+ outline: '1px solid',
+ outlineColor: 'rgb(0, 255, 0)',
+ margin: '20px',
+ padding: '10px',
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
+ fontWeight: 300,
+ fontSize: 14,
+ maxWidth: 600,
+ },
+ });
+ });
+
+ it('should work with array type', () => {
+ const result = styleFunctionSx({
+ theme: {
+ ...theme,
+ modularCssLayers: true,
+ },
+ sx: [
+ {
+ bgcolor: 'secondary.main',
+ },
+ {
+ color: 'primary.main',
+ },
+ ],
+ });
+
+ expect(result).to.deep.equal([
+ {
+ '@layer sx': {
+ backgroundColor: 'rgb(0, 255, 0)',
+ },
+ },
+ {
+ '@layer sx': {
+ color: 'rgb(0, 0, 255)',
+ },
+ },
+ ]);
+ });
+
+ it('should work with function type', () => {
+ const result = styleFunctionSx({
+ theme: {
+ ...theme,
+ modularCssLayers: true,
+ },
+ sx: (t) => ({
+ color: t.palette.primary.main,
+ bgcolor: t.palette.secondary.main,
+ }),
+ });
+
+ expect(result).to.deep.equal({
+ '@layer sx': {
+ color: 'rgb(0, 0, 255)',
+ backgroundColor: 'rgb(0, 255, 0)',
+ },
+ });
+ });
+
+ it('should work with nested sx', () => {
+ const result = styleFunctionSx({
+ theme: {
+ ...theme,
+ modularCssLayers: true,
+ },
+ sx: {
+ color: 'primary.main',
+ '&:hover': {
+ bgcolor: 'secondary.main',
+ },
+ },
+ });
+
+ expect(result).to.deep.equal({
+ '@layer sx': {
+ color: 'rgb(0, 0, 255)',
+ '&:hover': {
+ backgroundColor: 'rgb(0, 255, 0)',
+ },
+ },
+ });
+ });
+
+ it('should work with nested sx and function', () => {
+ const result = styleFunctionSx({
+ theme: {
+ ...theme,
+ modularCssLayers: true,
+ },
+ sx: {
+ color: 'primary.main',
+ '&:hover': (t) => ({
+ bgcolor: t.palette.secondary.main,
+ }),
+ },
+ });
+
+ expect(result).to.deep.equal({
+ '@layer sx': {
+ color: 'rgb(0, 0, 255)',
+ '&:hover': {
+ backgroundColor: 'rgb(0, 255, 0)',
+ },
+ },
+ });
+ });
+ });
});