Next.js Material UI Layout with Side Menu

Overview

This is a simple example of how to create a Material UI layout with an AppBar and Drawer side menu for the entire application. The pages are rendered inside the layout. This post uses Material UI Version 4.12.3. MUI Version 5 was released in September 2021. You can find out more about Material UI (MUI) releases here,

Source Code

GitHub Repo

Prerequisites

This example uses the template created by Melih Yumak. This template was referenced from the Next.js website. Please download from Melih’s github repository here. This is a huge time saver for adding material in Next.js.

After cloning or downloading the template, go into the directory and run

npm install

Create the application

If you want to do this from scratch. I recommend cloning the source files in case the library versions have changed. Plus the example application in GitHub has a few pages to navigate to. For more information on how to create a new Next.js app click here.

npx create-next-app@latest my-next-app

Install Material UIVersion 4

React-Material for the inputs and layout.

Note: Version 5 was released during the development of this article.

For version 4 documentation click here
For the current version documentation click here
All documentation for all versions here

npm install @material-ui/core@4.12.3 @material-ui/icons@4.11.2 
npm install @material-ui/lab@4.0.0-alpha.60 
npm install @material-ui/pickers@3.3.10

Running Next.js App

npm run dev

Layout Component

Create a folder labeled components and add a file labeled MainNav.js under the ./components/layout/” folder.

The component will use Material UIs AppBar, Drawer, Toolbar, ListItems, Button to generate the layout..

Copied source from Material-UI.com for AppBar and Drawer combo example.

The following is the source code for the ./components/MainNav.js” Component.

import React from 'react';
import clsx from 'clsx';
import { makeStyles, useTheme, createTheme } from '@material-ui/core/styles';
import Drawer from '@material-ui/core/Drawer';
import CssBaseline from '@material-ui/core/CssBaseline';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import List from '@material-ui/core/List';
import Typography from '@material-ui/core/Typography';
import Divider from '@material-ui/core/Divider';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft';
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import InboxIcon from '@material-ui/icons/MoveToInbox';
import LoginIcon from '@material-ui/icons/VpnKey';
import HomeIcon from '@material-ui/icons/Home';
import RegisterIcon from '@material-ui/icons/ContactMail';
import MenuItem from '@material-ui/core/MenuItem';
import MailIcon from '@material-ui/icons/Mail';
import Button from '@material-ui/core/Button';
import Link from '@/src/Link';
import { useRouter } from 'next/router'

import Container from '@material-ui/core/Container';
const drawerWidth = 240;

const useStyles = makeStyles((theme) => ({
  root: {
    display: 'flex',
  },
  title: {
    flexGrow: 1,
  },
  appBar: {
    transition: theme.transitions.create(['margin', 'width'], {
      easing: theme.transitions.easing.sharp,
      duration: theme.transitions.duration.leavingScreen,
    }),
  },
  appBarShift: {
    width: `calc(100% - ${drawerWidth}px)`,
    marginLeft: drawerWidth,
    transition: theme.transitions.create(['margin', 'width'], {
      easing: theme.transitions.easing.easeOut,
      duration: theme.transitions.duration.enteringScreen,
    }),
  },
  menuButton: {
    marginRight: theme.spacing(2),
  },
  hide: {
    display: 'none',
  },
  drawer: {
    width: drawerWidth,
    flexShrink: 0,
  },
  drawerPaper: {
    width: drawerWidth,
  },
  drawerHeader: {
    display: 'flex',
    alignItems: 'center',
    padding: theme.spacing(0, 1),
    // necessary for content to be below app bar
    ...theme.mixins.toolbar,
    justifyContent: 'flex-end',
  },
  content: {
    flexGrow: 1,
    padding: theme.spacing(3),
    transition: theme.transitions.create('margin', {
      easing: theme.transitions.easing.sharp,
      duration: theme.transitions.duration.leavingScreen,
    }),
    marginLeft: -drawerWidth,
  },
  contentShift: {
    transition: theme.transitions.create('margin', {
      easing: theme.transitions.easing.easeOut,
      duration: theme.transitions.duration.enteringScreen,
    }),
    marginLeft: 0,
  },
}));

const menuItems = [
  
]

export default function PersistentDrawerLeft(props) {
  const classes = useStyles();
  const theme = useTheme();
  const [open, setOpen] = React.useState(false);
  const router = useRouter();

  const handleDrawerOpen = () => {
    setOpen(true);
  };

  const handleDrawerClose = () => {
    setOpen(false);
  };

  const activeRoute = (routeName, currentRoute) => {
    return routeName === currentRoute? true : false;
  }

  const routes = [
    {
      id: 1, 
      label:'Home', 
      path: '/', 
      icon: HomeIcon
    }, 
    {
      id: 2, 
      label: 'Login', 
      path: '/login', 
      icon: LoginIcon
    }, 
    {
      id: 3, 
      label: 'Register', 
      path: '/register', 
      icon: RegisterIcon
    }
  ];

  return (
    <div className={classes.root}>
      <CssBaseline />
      <AppBar
        position="fixed"
        className={clsx(classes.appBar, {
          [classes.appBarShift]: open,
        })}
      >
        <Toolbar>
          <IconButton
            color="inherit"
            aria-label="open drawer"
            onClick={handleDrawerOpen}
            edge="start"
            className={clsx(classes.menuButton, open && classes.hide)}
          >
            <MenuIcon />
          </IconButton>
          <Typography variant="h6" className={classes.title}>React Material v4 Layout Example</Typography>                    
          <Button color="inherit" component={Link} href="/register">Register</Button>     
          <Button color="inherit" component={Link} href="/login">Login</Button>     

        </Toolbar>
      </AppBar>
      <Drawer
        className={classes.drawer}
        variant="persistent"
        anchor="left"
        open={open}
        classes={{
          paper: classes.drawerPaper,
        }}
      >
        <div className={classes.drawerHeader}>
          <IconButton onClick={handleDrawerClose}>
            {theme.direction === 'ltr' ? <ChevronLeftIcon /> : <ChevronRightIcon />}
          </IconButton>
        </div>
        <Divider />
        <List>
          {routes.map((item, index) => (
            <Link  href={item.path} style={{ textDecoration: 'none', color: 'black' }} key={index}>
              <MenuItem selected={activeRoute(item.path, router.pathname)}>
                <ListItem button key={index}  >
                  <ListItemIcon> <item.icon /> </ListItemIcon>
                  <ListItemText primary={item.label} />
                </ListItem>
              </MenuItem>
            </Link>
          ))}
        </List>

      </Drawer>
      <main
        className={clsx(classes.content, {
          [classes.contentShift]: open,
        })}
      >
        <div className={classes.drawerHeader} />
        <Container maxWidth="xl">
          {props.mainPage}
        </Container>

      </main>
    </div>
  );
}

Breakdown of Side Menu (drawer)

Highlighting Menu Item

Highlight the menu item with useRouter hook from next/router.

import { useRouter } from 'next/router'
...

export default function PersistentDrawerLeft(props) {
...
  const router = useRouter();

  const activeRoute = (routeName, currentRoute) => {
    return routeName === currentRoute? true : false;
  }
  
  const routes = [
    {
      id: 1, 
      label:'Home', 
      path: '/', 
      icon: HomeIcon
    }, 
    {
      id: 2, 
      label: 'Login', 
      path: '/login', 
      icon: LoginIcon
    }, 
    {
      id: 3, 
      label: 'Register', 
      path: '/register', 
      icon: RegisterIcon
    }
  ];

return (

...

 {routes.map((item, index) => (


     <Link  href={item.path} style={{ textDecoration: 'none', color: 'black' }} key={item.Id}>
       <MenuItem selected={activeRoute(item.path, router.pathname)}>
         <ListItem button key={item.id}  >
            <ListItemIcon> <item.icon /> </ListItemIcon>
                <ListItemText primary={item.label} />
            </ListItem>
         </MenuItem>
      </Link>

  ))}

Wrap the <ListItem> with a <Link> and <MenuItem> JSX tags.

<Link  href={item.path} style={{ textDecoration: 'none' }} key={item.Id}>
   <MenuItem selected={activeRoute(item.path, router.pathname)}>
      <ListItem button key={item.id}  >
          <ListItemIcon> <item.icon /> </ListItemIcon>
          <ListItemText primary={item.label} />
       </ListItem>
   </MenuItem>
</Link>

In _app.js pass the pageProps to the layout component/

<MainNav mainPage={<Component {...pageProps} />}/>

Showing the current route inside the layout

props.mainPage is passed to the layout component from _app.js

   </Drawer>
      <main
        className={clsx(classes.content, {
          [classes.contentShift]: open,
        })}
      >
        <div className={classes.drawerHeader} />
        <Container maxWidth="xl">
          {props.mainPage}
        </Container>

      </main>

When navigating between pages use the Link component with href for better performance. Without Link, the page will take an extra second or two while showing a new page loading spinner in the tab.

In the Next.JS template from MUI uses a custom Link function to handle navigation.

The option from Next.js is the following”

import Link from 'next/link'

The MUI Template Link is located under the root “./src” folder/

Source code for Link.jsOPTIONAL

import React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { useRouter } from 'next/router';
import NextLink from 'next/link';
import MuiLink from '@material-ui/core/Link';

const NextComposed = React.forwardRef(function NextComposed(props, ref) {
  const { as, href, ...other } = props;

  return (
    <NextLink href={href} as={as}>
      <a ref={ref} {...other} />
    </NextLink>
  );
});

NextComposed.propTypes = {
  as: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
  href: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
  prefetch: PropTypes.bool,
};

// A styled version of the Next.js Link component:
// https://nextjs.org/docs/#with-link
function Link(props) {
  const {
    href,
    activeClassName = 'active',
    className: classNameProps,
    innerRef,
    naked,
    ...other
  } = props;

  const router = useRouter();
  const pathname = typeof href === 'string' ? href : href.pathname;
  const className = clsx(classNameProps, {
    [activeClassName]: router.pathname === pathname && activeClassName,
  });

  if (naked) {
    return <NextComposed className={className} ref={innerRef} href={href} {...other} />;
  }

  return (
    <MuiLink component={NextComposed} className={className} ref={innerRef} href={href} {...other} />
  );
}

Link.propTypes = {
  activeClassName: PropTypes.string,
  as: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
  className: PropTypes.string,
  href: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
  innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
  naked: PropTypes.bool,
  onClick: PropTypes.func,
  prefetch: PropTypes.bool,
};

export default React.forwardRef((props, ref) => <Link {...props} innerRef={ref} />);
<Button color="inherit" component={Link} href="/login">Login</Button>  

Render the Current Page in the Layout

To render the page inside the layout. Under the closing <Drawer/> tag and between the <main> and optionally the <container> as shown below.

As an option to give the content different widths use Material UI’s Container.

...
     </Drawer>
      <main
        className={clsx(classes.content, {
          [classes.contentShift]: open,
        })}
      >
        <div className={classes.drawerHeader} />
        <Container maxWidth="xl">
          {props.mainPage}
        </Container>

      </main>

So passing the ..pageProps to the MainLayout component will allow you to render pages with the appbar at the top of every route.

{props.mainPage}

In the MainLayout.js component from above you just add the <Component .. /> with props.mainPage.

...
<Container>
    {props.mainPage}
</Container>
...

Add Layout Component to _app.js

Add the following source to _app.js

import React from 'react';
import PropTypes from 'prop-types';
import Head from 'next/head';
import { ThemeProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline';
import theme from '@/src/theme';
import MainNav from '@/components/main-layout/MainNav';

export default function MyApp(props) {
  const { Component, pageProps } = props;

  React.useEffect(() => {
    // Remove the server-side injected CSS.
    const jssStyles = document.querySelector('#jss-server-side');
    if (jssStyles) {
      jssStyles.parentElement.removeChild(jssStyles);
    }
  }, []);

  return (
    <React.Fragment>
      <Head>
        <title>My page</title>
        <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
      </Head>
      <ThemeProvider theme={theme}>
        
        {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
        <CssBaseline />
        <MainNav mainPage={<Component {...pageProps} />}/>
      </ThemeProvider>
    </React.Fragment>
  );
}

MyApp.propTypes = {
  Component: PropTypes.elementType.isRequired,
  pageProps: PropTypes.object.isRequired,
};

Setting up Direct Paths

Absolute Imports and Module Path Alias

Use Absolute Imports and Module Path Alias by creating jsconfig.json at the root folder. Get the full documentation here

A feature of Next.js is the option of defining direct paths.

{
    "compilerOptions": {
        "baseUrl": ".",
        "paths": {
            "@/components/*": ["components/*"],
            "@/src/*": ["src/*"],
            "@/styles/*": ["styles/*"]
        }
    }
}

Without Absolute Imports

import YourComponent from '../../../components/yourcomponent';

With Absolute Imports

import YourComponent from '@/components/yourcomponent';