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) => ( -