template nextjs latest app router with mui framework, responsive desktop and mobile
August 3rd 2024
how to create a Next.js app with the App Router using MUI (Material-UI) framework, including a responsive menu and SEO support. You can follow these steps to create the project yourself:
- Create a new Next.js project:
npx create-next-app@latest my-mui-nextjs-app
cd my-mui-nextjs-app
Choose the following options:
- Use TypeScript: Yes
- Use ESLint: Yes
- Use Tailwind CSS: No
- Use
src/
directory: Yes - Use App Router: Yes
- Customize default import alias: No
- Install MUI and required dependencies:
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
- Create a custom theme file
src/theme/theme.ts
:
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
// Customize your theme here
});
export default theme;
- Create a layout file
src/app/layout.tsx
:
import type { Metadata } from 'next'
import { AppRouterCacheProvider } from '@mui/material-nextjs/v13-appRouter';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import theme from '../theme/theme';
import Header from '../components/Header';
export const metadata: Metadata = {
title: 'My MUI Next.js App',
description: 'A responsive Next.js app with MUI',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<AppRouterCacheProvider>
<ThemeProvider theme={theme}>
<CssBaseline />
<Header />
{children}
</ThemeProvider>
</AppRouterCacheProvider>
</body>
</html>
)
}
- Create a Header component
src/components/Header.tsx
:
import React, { useState } from 'react';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Menu from '@mui/material/Menu';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import Link from 'next/link';
const Header = () => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [mobileMoreAnchorEl, setMobileMoreAnchorEl] = useState<null | HTMLElement>(null);
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleMobileMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
setMobileMoreAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
setMobileMoreAnchorEl(null);
};
const menuItems = [
{ label: 'All Categories', href: '/categories' },
{ label: 'News', href: '/news', subItems: [
{ label: 'Technology', href: '/news/technology' },
{ label: 'Business', href: '/news/business' },
]},
{ label: 'Blog', href: '/blog' },
{ label: 'About', href: '/about' },
];
const renderMenu = (
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
{menuItems.map((item) => (
<MenuItem key={item.label} onClick={handleMenuClose}>
<Link href={item.href}>
{item.label}
</Link>
</MenuItem>
))}
</Menu>
);
const renderMobileMenu = (
<Menu
anchorEl={mobileMoreAnchorEl}
open={Boolean(mobileMoreAnchorEl)}
onClose={handleMenuClose}
>
{menuItems.map((item) => (
<MenuItem key={item.label} onClick={handleMenuClose}>
<Link href={item.href}>
{item.label}
</Link>
</MenuItem>
))}
</Menu>
);
return (
<AppBar position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
My App
</Typography>
<Box sx={{ display: { xs: 'none', md: 'flex' } }}>
{menuItems.map((item) => (
<Button
key={item.label}
color="inherit"
onClick={item.subItems ? handleMenuOpen : undefined}
>
{item.label}
</Button>
))}
</Box>
<Box sx={{ display: { xs: 'flex', md: 'none' } }}>
<IconButton
size="large"
edge="end"
color="inherit"
aria-label="menu"
onClick={handleMobileMenuOpen}
>
<MenuIcon />
</IconButton>
</Box>
</Toolbar>
{renderMenu}
{renderMobileMenu}
</AppBar>
);
};
export default Header;
- Update
src/app/page.tsx
:
import Typography from '@mui/material/Typography';
import Container from '@mui/material/Container';
export default function Home() {
return (
<Container maxWidth="lg">
<Typography variant="h1" component="h1" gutterBottom>
Welcome to My MUI Next.js App
</Typography>
<Typography variant="body1">
This is a responsive Next.js app with MUI framework and App Router.
</Typography>
</Container>
)
}
- For SEO support, create a
src/app/sitemap.ts
file:
import { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://example.com',
lastModified: new Date(),
},
{
url: 'https://example.com/about',
lastModified: new Date(),
},
{
url: 'https://example.com/blog',
lastModified: new Date(),
},
]
}
- Create a
src/app/robots.ts
file:
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: '/private/',
},
sitemap: 'https://example.com/sitemap.xml',
}
}
This setup provides you with a basic Next.js 14 app using the App Router and MUI framework. It includes a responsive header with a desktop and mobile menu, and basic SEO support with metadata, sitemap, and robots.txt.
To complete your project:
- Create additional pages in the
src/app
directory for categories, news, blog, and about. - Implement the dropdown functionality for the News menu item.
- Add more detailed SEO metadata for each page.
- Style your components further using MUI's theming capabilities.
Remember to replace 'https://example.com' with your actual domain in the sitemap and robots files.
Github source code: nextjs-reactjs-mui-latest
if your want to have drawer on the left side that's always visible on desktop and toggleable on mobile
- First, let's update the
Header.tsx
component:
// 'use client'
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import React from 'react';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles';
interface HeaderProps {
onDrawerToggle: () => void;
}
const Header: React.FC<HeaderProps> = ({ onDrawerToggle }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
console.log('Header render...')
return (
<AppBar position="fixed" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}>
<Toolbar>
{isMobile && (
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={onDrawerToggle}
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
)}
<Typography variant="h6" noWrap component="div">
My App
</Typography>
</Toolbar>
</AppBar>
);
};
export default Header;
- Now, let's create a new
Drawer.tsx
component (app/components)
import Collapse from '@mui/material/Collapse';
import Drawer from '@mui/material/Drawer';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import Link from 'next/link';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import React from 'react';
import Toolbar from '@mui/material/Toolbar';
import useMediaQuery from '@mui/material/useMediaQuery';
import { usePathname } from 'next/navigation';
import { useTheme } from '@mui/material/styles';
interface DrawerProps {
open: boolean;
onClose: () => void;
}
const drawerWidth = 240;
const menuItems = [
{ label: 'Home', href: '/' },
{
label: 'News',
href: '/news',
subItems: [
{ label: 'Blockchain', href: '/news/blockchain' },
{ label: 'Technical', href: '/news/technical' },
{ label: 'Programming', href: '/news/programming' },
]
},
{ label: 'About', href: '/about' },
];
const DrawerContent: React.FC = () => {
const [openSubMenu, setOpenSubMenu] = React.useState('');
const pathname = usePathname();
const handleSubMenuToggle = (label: string) => {
setOpenSubMenu(openSubMenu === label ? '' : label);
};
return (
<List>
{menuItems.map((item) => (
<React.Fragment key={item.label}>
<ListItem disablePadding>
<ListItemButton
component={Link}
href={item.href}
selected={pathname === item.href}
onClick={() => item.subItems && handleSubMenuToggle(item.label)}
>
<ListItemText primary={item.label} />
{item.subItems && (openSubMenu === item.label ? <ExpandLess /> : <ExpandMore />)}
</ListItemButton>
</ListItem>
{item.subItems && (
<Collapse in={openSubMenu === item.label} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{item.subItems.map((subItem) => (
<ListItemButton
key={subItem.label}
sx={{ pl: 4 }}
component={Link}
href={subItem.href}
selected={pathname === subItem.href}
>
<ListItemText primary={subItem.label} />
</ListItemButton>
))}
</List>
</Collapse>
)}
</React.Fragment>
))}
</List>
);
};
const AppDrawer: React.FC<DrawerProps> = ({ open, onClose }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
return (
<Drawer
variant={isMobile ? 'temporary' : 'permanent'}
open={isMobile ? open : true}
onClose={onClose}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
sx={{
'& .MuiDrawer-paper': {
boxSizing: 'border-box',
width: drawerWidth,
},
}}
>
<Toolbar />
<DrawerContent />
</Drawer>
);
};
export default AppDrawer;
- Update the
layout.tsx
file to include the drawer:
'use client'
import AppDrawer from '../components/Drawer';
import { AppRouterCacheProvider } from '@mui/material-nextjs/v14-appRouter';
import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline';
import Header from '../components/Header';
import { ThemeProvider } from '@mui/material/styles';
import theme from '../theme/theme';
import { useState } from 'react';
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const [mobileOpen, setMobileOpen] = useState(false);
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
return (
<html lang="en">
<body>
<AppRouterCacheProvider>
<ThemeProvider theme={theme}>
<CssBaseline />
<Box sx={{ display: 'flex' }}>
<Header onDrawerToggle={handleDrawerToggle} />
<AppDrawer open={mobileOpen} onClose={handleDrawerToggle} />
<Box
component="main"
sx={{ flexGrow: 1, p: 3, width: { sm: `calc(100% - 240px)` } }}
>
<ThemeProvider theme={theme}>
<CssBaseline />
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
{children}
</Box>
</ThemeProvider>
</Box>
</Box>
</ThemeProvider>
</AppRouterCacheProvider>
</body>
</html>
)
}
- Update the
page.tsx
file to add some padding at the top:
import Box from '@mui/material/Box';
import Container from '@mui/material/Container';
import Typography from '@mui/material/Typography';
export default function Home() {
return (
<Box sx={{ mt: 8 }}>
<Container maxWidth="lg">
<Typography variant="h1" component="h1" gutterBottom>
Welcome to My MUI Next.js App
</Typography>
<Typography variant="body1">
This is a responsive Next.js app with MUI framework and App Router.
</Typography>
</Container>
</Box>
)
}
These changes will:
- Always show the drawer on desktop screens.
- Make the drawer toggleable on mobile screens.
- Update the menu structure to include News with submenu items for Blockchain, Technical, and Programming.
- Highlight the active link based on the current route.
- Ensure the content doesn't overlap with the app bar.
Remember to create the corresponding pages for each route in your src/app
directory (e.g., src/app/news/blockchain/page.tsx
, src/app/news/technical/page.tsx
, etc.).
This setup provides a responsive layout with a left-side drawer that adapts to desktop and mobile views, includes nested menu items, and highlights the active link. The SEO support from the previous example is still in place.
Github source code: on branch Drawer_Mobile_toggleMenu