How to Implement Material UI to A Next.js App Router Project

Tutorial
Front End
Blog
Studiofru
Studiofru
Cover Image for How to Implement Material UI to A Next.js App Router Project

As you may realize, Material UI documentation guide doesn't really give us the comprehensive information on how to implement the framework within the Next.js app router project. The documentation simply guides us on how to implement the frameworks to a Next.js pages router. But, it doesn’t mean that it is impossible and hard to implement it in a Next.js app router. It’s just a bit tricky, especially for the newbies.

We normally only need to install some dependencies within our pages router project and directly implement the components and themes on the _app.js file. 

Install the dependencies:

npm install @mui/material @emotion/react @emotion/styled @mui/styled-engine-sc styled-components 

Implement Material UI inside the _app.js file

import React, { useEffect } from 'react'
import useMediaQuery from '@mui/material/useMediaQuery';
import { ThemeProvider, createTheme, responsiveFontSizes } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';


function MyApp({ Component, pageProps }) {
  const colorMode = React.useMemo(
    () =>
      createTheme({
        palette: {
          mode: 'dark',
  },             
      }),
  );


const theme = responsiveFontSizes(colorMode);


  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
        <Component {...pageProps} />
    </ThemeProvider>
  )
}


export default MyApp

Well, that’s not the case with the implementation of Material UI in the Next.js app router project. We need to install more dependencies, create some Typescript files and implement the theme inside the layout.tsx file. Here is the complete guide in how to do it.

Install the dependencies:

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

Create ThemeRegistry folder inside the components folder and create the EmotionCache.tsx file and copy paste the code below.

'use client';
import * as React from 'react';
import createCache from '@emotion/cache';
import { useServerInsertedHTML } from 'next/navigation';
import { CacheProvider as DefaultCacheProvider } from '@emotion/react';
import type { EmotionCache, Options as OptionsOfCreateCache } from '@emotion/cache';


export type NextAppDirEmotionCacheProviderProps = {
  /** This is the options passed to createCache() from 'import createCache from "@emotion/cache"' */
  options: Omit<OptionsOfCreateCache, 'insertionPoint'>;
  /** By default <CacheProvider /> from 'import { CacheProvider } from "@emotion/react"' */
  CacheProvider?: (props: {
    value: EmotionCache;
    children: React.ReactNode;
  }) => React.JSX.Element | null;
  children: React.ReactNode;
};


// Adapted from https://github.com/garronej/tss-react/blob/main/src/next/appDir.tsx
export default function NextAppDirEmotionCacheProvider(props: NextAppDirEmotionCacheProviderProps) {
  const { options, CacheProvider = DefaultCacheProvider, children } = props;


  const [registry] = React.useState(() => {
    const cache = createCache(options);
    cache.compat = true;
    const prevInsert = cache.insert;
    let inserted: { name: string; isGlobal: boolean }[] = [];
    cache.insert = (...args) => {
      const [selector, serialized] = args;
      if (cache.inserted[serialized.name] === undefined) {
        inserted.push({
          name: serialized.name,
          isGlobal: !selector,
        });
      }
      return prevInsert(...args);
    };
    const flush = () => {
      const prevInserted = inserted;
      inserted = [];
      return prevInserted;
    };
    return { cache, flush };
  });


  useServerInsertedHTML(() => {
    const inserted = registry.flush();
    if (inserted.length === 0) {
      return null;
    }
    let styles = '';
    let dataEmotionAttribute = registry.cache.key;


    const globals: {
      name: string;
      style: string;
    }[] = [];


    inserted.forEach(({ name, isGlobal }) => {
      const style = registry.cache.inserted[name];


      if (typeof style !== 'boolean') {
        if (isGlobal) {
          globals.push({ name, style });
        } else {
          styles += style;
          dataEmotionAttribute += ` ${name}`;
        }
      }
    });


    return (
      <React.Fragment>
        {globals.map(({ name, style }) => (
          <style
            key={name}
            data-emotion={`${registry.cache.key}-global ${name}`}
            // eslint-disable-next-line react/no-danger
            dangerouslySetInnerHTML={{ __html: style }}
          />
        ))}
        {styles && (
          <style
            data-emotion={dataEmotionAttribute}
            // eslint-disable-next-line react/no-danger
            dangerouslySetInnerHTML={{ __html: styles }}
          />
        )}
      </React.Fragment>
    );
  });


  return <CacheProvider value={registry.cache}>{children}</CacheProvider>;
}

Secondly, create theme.ts file inside the ThemeRegistry folder

import { createTheme } from '@mui/material/styles';
import {  green, cyan } from '@mui/material/colors';


// Override Mui's theme typings to include the new theme property
declare module '@mui/material/styles/createTheme' {
  interface Theme {
    status: {
      danger: React.CSSProperties['color'],
    }
  }
  interface ThemeOptions {
    status?: {
      danger?: React.CSSProperties['color']
    }
  }
}


const theme = createTheme({
  palette: {
    mode: 'dark',
    primary: {
      main: green [100]
     },
      secondary: {
            main: cyan [200]
          },
      background: {
            default: '#01132C',
            paper: '#035146',
          },
  },          
  components: {
    MuiAlert: {
      styleOverrides: {
        root: ({ ownerState }) => ({
          ...(ownerState.severity === 'info' && {
            backgroundColor: '#60a5fa',
          }),
        }),
      },
    },
  },
});


export default theme;

Thirdly, create the ThemeRegistry.tsx file inside the ThemeRegistry folder and copy paste this code below into it.

'use client';
import * as React from 'react';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import NextAppDirEmotionCacheProvider from './EmotionCache';
import theme from './theme';

export default function ThemeRegistry({ children }: { children: React.ReactNode }) {
  return (
    <NextAppDirEmotionCacheProvider options={{ key: 'mui' }}>
      <ThemeProvider theme={theme}>
        {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
        <CssBaseline />
        {children}
      </ThemeProvider>
    </NextAppDirEmotionCacheProvider>
  );
}

Lastly, don’t forget to inject the ThemeRegistry that we have created into the layout.tsx file.

import "@/styles/globals.css";
import ThemeRegistry from '@/components/ThemeRegistry/ThemeRegistry';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
          <ThemeRegistry>
            <div className="min-h-screen">
              <main>{children}</main>
            </div>     
          </ThemeRegistry>  
      </body>
    </html>
  );
}

External Link
Click here

Share this article