Skip to content
+

Next.js integration

Learn how to use Material UI with Next.js.

App Router

This section walks through the Material UI integration with the Next.js App Router, an evolution of the Pages Router, and, currently, the recommended way of building new Next.js applications starting from version 13.

Installing the dependencies

Start by ensuring that you already have @mui/material and next installed. Then, run one of the following commands to install the dependencies:

npm install @mui/material-nextjs @emotion/cache

Configuration

Inside app/layout.tsx, import the AppRouterCacheProvider and wrap all elements under the <body> with it:

app/layout.tsx
+import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
 // or `v1X-appRouter` if you are using Next.js v1X

 export default function RootLayout(props) {
   return (
     <html lang="en">
       <body>
+        <AppRouterCacheProvider>
           {props.children}
+        </AppRouterCacheProvider>
       </body>
     </html>
   );
 }

Custom cache (optional)

Use the options prop to override the default cache options—for example, the code snippet below shows how to change the CSS key to css (the default is mui):

  <AppRouterCacheProvider
+   options={{ key: 'css' }}
  >
    {children}
  </AppRouterCacheProvider>

Font optimization

To integrate Next.js font optimization with Material UI, create a new file with the 'use client'; directive. Then create a theme using var(--font-roboto) as a value for the typography.fontFamily field.

src/theme.ts
'use client';
import { createTheme } from '@mui/material/styles';

const theme = createTheme({
  typography: {
    fontFamily: 'var(--font-roboto)',
  },
});

export default theme;

Finally, in src/app/layout.tsx, pass the theme to the ThemeProvider:

app/layout.tsx
 import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
+import { Roboto } from 'next/font/google';
+import { ThemeProvider } from '@mui/material/styles';
+import theme from '../theme';

+const roboto = Roboto({
+  weight: ['300', '400', '500', '700'],
+  subsets: ['latin'],
+  display: 'swap',
+  variable: '--font-roboto',
+});

 export default function RootLayout(props) {
   const { children } = props;
   return (
+    <html lang="en" className={roboto.variable}>
       <body>
          <AppRouterCacheProvider>
+           <ThemeProvider theme={theme}>
              {children}
+           </ThemeProvider>
          </AppRouterCacheProvider>
       </body>
     </html>
   );
 }

To learn more about theming, check out the theming guide page.

CSS theme variables

To use CSS theme variables, enable the cssVariables flag:

src/theme.ts
 'use client';
 const theme = createTheme({
+  cssVariables: true,
 });

Learn more about the advantages of CSS theme variables and how to prevent SSR flickering.

Using other styling solutions

If you are using a styling solution other than Emotion to customize Material UI components, set enableCssLayer: true in the options prop:

<AppRouterCacheProvider options={{ enableCssLayer: true }}>

This option ensures that the styles generated by Material UI will be wrapped in a CSS @layer mui rule, which is overridden by anonymous layer styles when using Material UI with CSS Modules, Tailwind CSS, or even plain CSS without using @layer.

To learn more about it, see the MDN CSS layer documentation.

Next.js v16 Client Component restriction

If you encounter Functions cannot be passed directly to Client Components error from passing Next.js Link to Material UI component prop, you need to create a wrapper component with use client directive like the following:

src/components/Link.tsx
'use client';
import Link, { LinkProps } from 'next/link';

export default Link;

Then, replace the Next.js Link with the wrapper component:

src/app/page.tsx
- import Link from 'next/link';
+ import Link from '../components/Link';
  ...
  <Button component={Link} href="/about" variant="contained">
    Go to About Page
  </Button>

URL-driven UI and the Suspense boundary

When client components use Next.js App Router hooks that read the URL—for example useSearchParams() from next/navigation for filters, tabs, or pagination—Next.js expects a <Suspense> boundary around that part of the React tree. Without it, you may see build failures or runtime messages about a missing Suspense boundary (behavior depends on your Next.js version and static vs dynamic rendering).

This pattern is common with Material UI: Table, Tabs, TextField, and other controls are often implemented as client components that sync to the query string.

Recommended structure: keep page.tsx as a server component when possible, and wrap only the client subtree that calls useSearchParams in <Suspense>.

Avoid fallback={null} (or an empty fallback) for UI that reserves space in the layout (toolbars, filters, tab bars, and similar). The server and the initial streamed HTML then omit that subtree, and the real content appears only after the client hydrates, which often causes layout shift and hurts CLS. Prefer a fallback whose size and structure approximate the final UI, for example Material UI Skeleton inside Stack or Box with the same minHeight, flex direction, and breakpoints as the loaded component.

app/orders/page.tsx
import { Suspense } from 'react';
import Box from '@mui/material/Box';
import Skeleton from '@mui/material/Skeleton';
import Stack from '@mui/material/Stack';
import OrdersToolbar from './OrdersToolbar';

function OrdersToolbarFallback() {
  return (
    <Stack
      direction="row"
      spacing={2}
      useFlexGap
      sx={{ flexWrap: 'wrap', alignItems: 'center', minHeight: 56 }}
    >
      <Skeleton
        variant="rounded"
        height={40}
        sx={{ minWidth: 200, flexGrow: { xs: 1, sm: 0 } }}
      />
      <Skeleton variant="rounded" width={120} height={40} />
      <Box sx={{ flexGrow: 1 }} />
      <Skeleton variant="rounded" width={100} height={40} />
    </Stack>
  );
}

export default function Page() {
  return (
    <Suspense fallback={<OrdersToolbarFallback />}>
      <OrdersToolbar />
    </Suspense>
  );
}

OrdersToolbar would be a file marked with 'use client' that calls useSearchParams() and renders Material UI components. Adjust the fallback's layout and Skeleton sizes so they match your real toolbar (or filter row) as closely as possible.

For details and version-specific notes, see the Next.js documentation for useSearchParams.

Pages Router

This section walks through the Material UI integration with the Next.js Pages Router, for both Server-side Rendering (SSR) and Static Site Generation (SSG).

Installing the dependencies

Start by ensuring that you already have @mui/material and next installed. Then, run one of the following commands to install the dependencies:

npm install @mui/material-nextjs @emotion/cache @emotion/server

Configuration

Inside the pages/_document.tsx file:

  • Import documentGetInitialProps and use it as the Document's getInitialProps.
  • Import DocumentHeadTags and render it inside the <Head>.
pages/_document.tsx
+import {
+  DocumentHeadTags,
+  documentGetInitialProps,
+} from '@mui/material-nextjs/v15-pagesRouter';
 // or `v1X-pagesRouter` if you are using Next.js v1X

 export default function MyDocument(props) {
   return (
     <Html lang="en">
       <Head>
+        <DocumentHeadTags {...props} />
         ...
       </Head>
       <body>
         <Main />
         <NextScript />
       </body>
     </Html>
   );
 }

+MyDocument.getInitialProps = async (ctx) => {
+  const finalProps = await documentGetInitialProps(ctx);
+  return finalProps;
+};

Then, inside pages/_app.tsx, import the AppCacheProvider component and render it as the root element:

pages/_app.tsx
+import { AppCacheProvider } from '@mui/material-nextjs/v15-pagesRouter';
 // Or `v1X-pages` if you are using Next.js v1X

 export default function MyApp(props) {
   return (
+    <AppCacheProvider {...props}>
       <Head>
         ...
       </Head>
       ...
+    </AppCacheProvider>
   );
 }

Custom cache (optional)

To use a custom Emotion cache, pass it to the emotionCache property in _document.tsx:

pages/_document.tsx
 ...

 MyDocument.getInitialProps = async (ctx) => {
   const finalProps = await documentGetInitialProps(ctx, {
+    emotionCache: createCustomCache(),
   });
   return finalProps;
 };

Cascade layers (optional)

To enable cascade layers (@layer), create a new cache with enableCssLayer: true and pass it to the emotionCache property in both _document.tsx and _app.tsx:

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;
 };
pages/_app.tsx
+import { createEmotionCache } from '@mui/material-nextjs/v15-pagesRouter';
  ...

const clientCache = createEmotionCache({ enableCssLayer: true });

+ export default function MyApp({ emotionCache = clientCache }) {
    return (
+     <AppCacheProvider emotionCache={emotionCache}>
        <Head>
          ...
        </Head>
        ...
      </AppCacheProvider>
    );
  }

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.

Each plugin must have the following properties:

  • enhanceApp: a higher-order component that receives the App component and returns a new app component.
  • resolveProps: a function that receives the initial props and returns a new props object.

When run, enhanceApp from each plugin is called first, from top to bottom, and then the process is repeated for resolveProps.

import { ServerStyleSheet } from 'styled-components';

MyDocument.getInitialProps = async (ctx) => {
  const jssSheets = new JSSServerStyleSheets();
  const styledComponentsSheet = new ServerStyleSheet();

  try {
    const finalProps = await documentGetInitialProps(ctx, {
      emotionCache: createEmotionCache(),
      plugins: [
        {
          // styled-components
          enhanceApp: (App) => (props) =>
            styledComponentsSheet.collectStyles(<App {...props} />),
          resolveProps: async (initialProps) => ({
            ...initialProps,
            styles: [
              styledComponentsSheet.getStyleElement(),
              ...initialProps.styles,
            ],
          }),
        },
        {
          // JSS
          enhanceApp: (App) => (props) => jssSheets.collect(<App {...props} />),
          resolveProps: async (initialProps) => {
            const css = jssSheets.toString();
            return {
              ...initialProps,
              styles: [
                ...initialProps.styles,
                <style
                  id="jss-server-side"
                  key="jss-server-side"
                  // eslint-disable-next-line react/no-danger
                  dangerouslySetInnerHTML={{ __html: css }}
                />,
                <style id="insertion-point-jss" key="insertion-point-jss" />,
              ],
            };
          },
        },
      ],
    });
    return finalProps;
  } finally {
    styledComponentsSheet.seal();
  }
};

TypeScript

If you are using TypeScript, add DocumentHeadTagsProps to the Document's props interface:

+import type { DocumentHeadTagsProps } from '@mui/material-nextjs/v15-pagesRouter';
 // or `v1X-pagesRouter` if you are using Next.js v1X

+export default function MyDocument(props: DocumentProps & DocumentHeadTagsProps) {
   ...
 }

Font optimization

To integrate Next.js font optimization with Material UI, open pages/_app.tsx and create a theme using var(--font-roboto) as a value for the typography.fontFamily field.

pages/_app.tsx
 import * as React from 'react';
 import Head from 'next/head';
 import { AppProps } from 'next/app';
 import { AppCacheProvider } from '@mui/material-nextjs/v15-pagesRouter';
+import { ThemeProvider, createTheme } from '@mui/material/styles';
+import { Roboto } from 'next/font/google';

+const roboto = Roboto({
+  weight: ['300', '400', '500', '700'],
+  subsets: ['latin'],
+  display: 'swap',
+  variable: '--font-roboto',
+});

+const theme = createTheme({
+  typography: {
+    fontFamily: 'var(--font-roboto)',
+  },
+});

 export default function MyApp(props: AppProps) {
  const { Component, pageProps } = props;
  return (
    <AppCacheProvider {...props}>
      <Head>...</Head>
+     <ThemeProvider theme={theme}>
+       <main className={roboto.variable}>
          <Component {...pageProps} />
+       </main>
+     </ThemeProvider>
    </AppCacheProvider>
  );
 }

To learn more about theming, check out the Theming guide.

CSS theme variables

To use CSS theme variables, enable the cssVariables flag:

src/theme.ts
 'use client';
 const theme = createTheme({
+  cssVariables: true,
 });

Learn more about the advantages of CSS theme variables and how to prevent SSR flickering.