Building a Responsive Sidebar with React MUI Drawer: A Complete Guide
As a front-end developer, creating responsive navigation is one of the most common tasks you'll face. The Material UI Drawer component offers a powerful solution for implementing sidebars that work across devices, but mastering its nuances takes time. After building countless navigation systems, I've learned that the details make all the difference.
In this guide, I'll walk you through creating a responsive sidebar with MUI's Drawer component that toggles smoothly, works on all screen sizes, and follows best practices. We'll cover everything from basic implementation to advanced customization techniques that will elevate your UI.
Learning Objectives
By the end of this tutorial, you'll be able to:
- Implement a responsive MUI Drawer that adapts to different screen sizes
- Create a toggle mechanism for opening and closing the sidebar
- Customize the Drawer's appearance using MUI's styling system
- Implement proper accessibility features for navigation
- Handle common edge cases and performance considerations
- Structure your sidebar navigation with nested items and proper routing
Understanding the MUI Drawer Component
The Drawer component is a panel that slides in from the edge of the screen, typically used for navigation in responsive layouts. Before diving into implementation, let's explore what makes this component tick.
Drawer Variants and Behavior
MUI's Drawer comes in three main variants that determine its behavior:
-
Temporary - Appears over content with a backdrop when opened, closes when the backdrop is clicked. This is ideal for mobile views.
-
Persistent - Stays open until explicitly closed, pushing content to the side. This works well for tablet-sized screens where you want the drawer to be dismissible but not constantly hidden.
-
Permanent - Always visible and cannot be closed. Perfect for desktop layouts where you have ample screen space.
The beauty of MUI's Drawer lies in its flexibility - you can switch between these variants based on screen size, creating a truly responsive experience.
Core Props and Configuration
Let's explore the essential props that control the Drawer's behavior:
Prop | Type | Default | Description |
---|---|---|---|
anchor | 'left' | 'top' | 'right' | 'bottom' | 'left' | The edge from which the drawer slides in |
open | boolean | false | If true, the drawer is open |
variant | 'permanent' | 'persistent' | 'temporary' | 'temporary' | The variant to use |
onClose | function | - | Callback fired when the drawer requests to be closed |
elevation | number | 16 | The elevation of the drawer |
PaperProps | object | Props applied to the Paper element | |
ModalProps | object | Props applied to the Modal element (temporary variant only) | |
sx | object | The system prop that allows defining system overrides |
The Drawer component uses controlled behavior through the open
prop, which determines whether the drawer is visible. For temporary drawers, the onClose
callback is crucial for handling backdrop clicks and escape key presses.
Drawer Structure and Composition
The Drawer is essentially a container that can hold any content, but it's typically structured with:
- A header section (often with a logo and close button)
- A main navigation list (usually with
List
andListItem
components) - Optional footer content (like user info or logout button)
Under the hood, the Drawer renders a Paper
component inside a Modal
(for temporary variant) or directly in the DOM (for persistent and permanent variants). This is why props like PaperProps
are available for customization.
Setting Up Your Project
Before we start building our responsive sidebar, let's set up a basic React project with Material UI installed.
Creating a New React Project
If you don't already have a React project, create one using Create React App:
npx create-react-app responsive-sidebar
cd responsive-sidebar
Installing Material UI Dependencies
Now, let's install the required Material UI packages:
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled
These packages provide the core MUI components, icons, and the styling solution that MUI uses.
Building a Basic Drawer Component
Let's start by creating a simple drawer that can toggle open and closed. We'll build on this foundation throughout the guide.
Creating the Basic Structure
First, create a new file called Sidebar.js
in your src
folder:
import React, { useState } from 'react';
import {
Box,
Drawer,
IconButton,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Divider,
Toolbar,
Typography,
} from '@mui/material';
import {
Menu as MenuIcon,
Home as HomeIcon,
Mail as MailIcon,
Settings as SettingsIcon,
} from '@mui/icons-material';
const Sidebar = () => {
const [open, setOpen] = useState(false);
const toggleDrawer = () => {
setOpen(!open);
};
const drawerContent = (
<Box sx={{ width: 250 }}>
<Toolbar>
<Typography variant="h6" component="div">
My App
</Typography>
</Toolbar>
<Divider />
<List>
<ListItem disablePadding>
<ListItemButton>
<ListItemIcon>
<HomeIcon />
</ListItemIcon>
<ListItemText primary="Home" />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton>
<ListItemIcon>
<MailIcon />
</ListItemIcon>
<ListItemText primary="Messages" />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
<ListItemText primary="Settings" />
</ListItemButton>
</ListItem>
</List>
</Box>
);
return (
<>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={toggleDrawer}
edge="start"
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
<Drawer anchor="left" open={open} onClose={toggleDrawer}>
{drawerContent}
</Drawer>
</>
);
};
export default Sidebar;
This gives us a basic drawer that opens when the menu icon is clicked and closes when clicking outside it. Let's break down the key elements:
- We use the
useState
hook to track whether the drawer is open or closed. - The
toggleDrawer
function toggles the drawer's state. - We define the drawer's content as a separate variable for better organization.
- The
Drawer
component uses theopen
state andonClose
handler to manage its visibility.
Integrating the Sidebar into Your App
Now, let's modify App.js
to include our sidebar:
import React from 'react';
import { AppBar, Toolbar, Typography, Box, CssBaseline } from '@mui/material';
import Sidebar from './Sidebar';
function App() {
return (
<Box sx={{ display: 'flex' }}>
<CssBaseline />
<AppBar position="fixed">
<Toolbar>
<Sidebar />
<Typography variant="h6" noWrap component="div">
Responsive Drawer
</Typography>
</Toolbar>
</AppBar>
<Box
component="main"
sx={{ flexGrow: 1, p: 3, mt: 8 }}
>
<Typography paragraph>
Your main content goes here. Try opening the sidebar by clicking the menu icon.
</Typography>
</Box>
</Box>
);
}
export default App;
In this setup, we've placed the sidebar toggle button in the AppBar and added some basic content to the main area.
Creating a Responsive Sidebar
Now that we have a basic drawer working, let's enhance it to be responsive. The key idea is to:
- Use a permanent drawer on larger screens
- Switch to a temporary drawer on smaller screens
- Adjust the layout accordingly
Adding Responsiveness with useMediaQuery
MUI provides a useMediaQuery
hook that helps us detect screen size changes. Let's use it to make our sidebar responsive:
import React, { useState } from 'react';
import {
Box,
Drawer,
AppBar,
Toolbar,
List,
Typography,
Divider,
IconButton,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
useTheme,
useMediaQuery,
} from '@mui/material';
import {
Menu as MenuIcon,
Home as HomeIcon,
Mail as MailIcon,
Settings as SettingsIcon,
} from '@mui/icons-material';
// Drawer width constant
const drawerWidth = 240;
const ResponsiveSidebar = () => {
const [mobileOpen, setMobileOpen] = useState(false);
const theme = useTheme();
const isLargeScreen = useMediaQuery(theme.breakpoints.up('md'));
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
const drawerContent = (
<>
<Toolbar>
<Typography variant="h6" noWrap component="div">
My App
</Typography>
</Toolbar>
<Divider />
<List>
<ListItem disablePadding>
<ListItemButton>
<ListItemIcon>
<HomeIcon />
</ListItemIcon>
<ListItemText primary="Home" />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton>
<ListItemIcon>
<MailIcon />
</ListItemIcon>
<ListItemText primary="Messages" />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
<ListItemText primary="Settings" />
</ListItemButton>
</ListItem>
</List>
</>
);
return (
<Box sx={{ display: 'flex' }}>
<AppBar
position="fixed"
sx={{
width: { md: `calc(100% - ${drawerWidth}px)` },
ml: { md: `${drawerWidth}px` },
}}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { md: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div">
Responsive Drawer
</Typography>
</Toolbar>
</AppBar>
{/* Mobile drawer (temporary) */}
<Box
component="nav"
sx={{ width: { md: drawerWidth }, flexShrink: { md: 0 } }}
>
<Drawer
variant="temporary"
open={mobileOpen}
onClose={handleDrawerToggle}
ModalProps={{
keepMounted: true, // Better open performance on mobile
}}
sx={{
display: { xs: 'block', md: 'none' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
>
{drawerContent}
</Drawer>
{/* Desktop drawer (permanent) */}
<Drawer
variant="permanent"
sx={{
display: { xs: 'none', md: 'block' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
open
>
{drawerContent}
</Drawer>
</Box>
{/* Main content */}
<Box
component="main"
sx={{
flexGrow: 1,
p: 3,
width: { md: `calc(100% - ${drawerWidth}px)` },
marginTop: '64px', // Height of AppBar
}}
>
<Typography paragraph>
This is a responsive sidebar example. On mobile, the drawer can be toggled.
On desktop, it's always visible.
</Typography>
<Typography paragraph>
Try resizing your browser window to see how it responds to different screen sizes.
</Typography>
</Box>
</Box>
);
};
export default ResponsiveSidebar;
Let's break down what's happening in this responsive implementation:
- We use
useMediaQuery
with the theme's breakpoint system to detect if we're on a large screen (md
breakpoint and up). - We render two different Drawer components:
- A temporary drawer for mobile screens that shows when
mobileOpen
is true - A permanent drawer for desktop screens that's always visible
- A temporary drawer for mobile screens that shows when
- We use the
sx
prop with responsive values to adjust the layout based on screen size:- The AppBar width and margin change on larger screens to accommodate the permanent drawer
- The main content area also adjusts its width accordingly
- The menu button only appears on mobile screens where the drawer is hidden by default
Updating App.js to Use the Responsive Sidebar
Now, let's update our App.js
to use the new responsive sidebar:
import React from 'react';
import { CssBaseline } from '@mui/material';
import ResponsiveSidebar from './ResponsiveSidebar';
function App() {
return (
<>
<CssBaseline />
<ResponsiveSidebar />
</>
);
}
export default App;
This implementation now gives us a sidebar that:
- Is always visible on desktop (md breakpoint and up)
- Can be toggled on mobile devices
- Adjusts the layout of the app accordingly
Enhancing the Drawer with Advanced Features
Now that we have a working responsive drawer, let's enhance it with more advanced features like nested navigation items, active state highlighting, and proper routing integration.
Adding Nested Navigation Items
Real-world applications often have hierarchical navigation. Let's implement nested navigation items:
import React, { useState } from 'react';
import {
Box,
Drawer,
AppBar,
Toolbar,
List,
Typography,
Divider,
IconButton,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Collapse,
useTheme,
useMediaQuery,
} from '@mui/material';
import {
Menu as MenuIcon,
Home as HomeIcon,
Mail as MailIcon,
Settings as SettingsIcon,
ExpandLess,
ExpandMore,
People as PeopleIcon,
Person as PersonIcon,
Group as GroupIcon,
} from '@mui/icons-material';
const drawerWidth = 240;
const ResponsiveSidebar = () => {
const [mobileOpen, setMobileOpen] = useState(false);
const [openSubmenu, setOpenSubmenu] = useState(false);
const theme = useTheme();
const isLargeScreen = useMediaQuery(theme.breakpoints.up('md'));
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
const handleSubmenuToggle = () => {
setOpenSubmenu(!openSubmenu);
};
const drawerContent = (
<>
<Toolbar>
<Typography variant="h6" noWrap component="div">
My App
</Typography>
</Toolbar>
<Divider />
<List>
<ListItem disablePadding>
<ListItemButton>
<ListItemIcon>
<HomeIcon />
</ListItemIcon>
<ListItemText primary="Dashboard" />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton>
<ListItemIcon>
<MailIcon />
</ListItemIcon>
<ListItemText primary="Messages" />
</ListItemButton>
</ListItem>
{/* Nested navigation item */}
<ListItem disablePadding>
<ListItemButton onClick={handleSubmenuToggle}>
<ListItemIcon>
<PeopleIcon />
</ListItemIcon>
<ListItemText primary="Users" />
{openSubmenu ? <ExpandLess /> : <ExpandMore />}
</ListItemButton>
</ListItem>
<Collapse in={openSubmenu} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
<ListItemButton sx={{ pl: 4 }}>
<ListItemIcon>
<PersonIcon />
</ListItemIcon>
<ListItemText primary="User Profiles" />
</ListItemButton>
<ListItemButton sx={{ pl: 4 }}>
<ListItemIcon>
<GroupIcon />
</ListItemIcon>
<ListItemText primary="User Groups" />
</ListItemButton>
</List>
</Collapse>
<ListItem disablePadding>
<ListItemButton>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
<ListItemText primary="Settings" />
</ListItemButton>
</ListItem>
</List>
</>
);
return (
<Box sx={{ display: 'flex' }}>
<AppBar
position="fixed"
sx={{
width: { md: `calc(100% - ${drawerWidth}px)` },
ml: { md: `${drawerWidth}px` },
}}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { md: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div">
Responsive Drawer with Nested Navigation
</Typography>
</Toolbar>
</AppBar>
<Box
component="nav"
sx={{ width: { md: drawerWidth }, flexShrink: { md: 0 } }}
>
<Drawer
variant="temporary"
open={mobileOpen}
onClose={handleDrawerToggle}
ModalProps={{
keepMounted: true,
}}
sx={{
display: { xs: 'block', md: 'none' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
>
{drawerContent}
</Drawer>
<Drawer
variant="permanent"
sx={{
display: { xs: 'none', md: 'block' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
open
>
{drawerContent}
</Drawer>
</Box>
<Box
component="main"
sx={{
flexGrow: 1,
p: 3,
width: { md: `calc(100% - ${drawerWidth}px)` },
marginTop: '64px',
}}
>
<Typography paragraph>
This drawer now includes nested navigation items. Click on "Users" to see the submenu.
</Typography>
<Typography paragraph>
In a real application, you would connect these navigation items to your routing system.
</Typography>
</Box>
</Box>
);
};
export default ResponsiveSidebar;
In this enhanced version:
- We've added a nested navigation structure under "Users" using the
Collapse
component - The submenu can be toggled open and closed with its own state
- The nested items are indented using the
pl
(padding-left) prop for better visual hierarchy
Integrating with React Router
For a complete sidebar, we need to integrate it with a routing system. Let's use React Router:
First, install React Router:
npm install react-router-dom
Now, let's update our sidebar to work with React Router:
import React, { useState } from 'react';
import { BrowserRouter, Routes, Route, Link, useLocation } from 'react-router-dom';
import {
Box,
Drawer,
AppBar,
Toolbar,
List,
Typography,
Divider,
IconButton,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Collapse,
useTheme,
useMediaQuery,
} from '@mui/material';
import {
Menu as MenuIcon,
Home as HomeIcon,
Mail as MailIcon,
Settings as SettingsIcon,
ExpandLess,
ExpandMore,
People as PeopleIcon,
Person as PersonIcon,
Group as GroupIcon,
} from '@mui/icons-material';
const drawerWidth = 240;
// Individual page components
const Dashboard = () => <Typography variant="h4">Dashboard Page</Typography>;
const Messages = () => <Typography variant="h4">Messages Page</Typography>;
const UserProfiles = () => <Typography variant="h4">User Profiles Page</Typography>;
const UserGroups = () => <Typography variant="h4">User Groups Page</Typography>;
const Settings = () => <Typography variant="h4">Settings Page</Typography>;
// Navigation item component with active state
const NavItem = ({ to, icon, primary, onClick, nested = false }) => {
const location = useLocation();
const isActive = location.pathname === to;
return (
<ListItem disablePadding>
<ListItemButton
component={Link}
to={to}
onClick={onClick}
sx={{
pl: nested ? 4 : 2,
bgcolor: isActive ? 'rgba(0, 0, 0, 0.08)' : 'transparent',
'&:hover': {
bgcolor: isActive ? 'rgba(0, 0, 0, 0.12)' : 'rgba(0, 0, 0, 0.04)',
}
}}
>
<ListItemIcon sx={{ color: isActive ? 'primary.main' : 'inherit' }}>
{icon}
</ListItemIcon>
<ListItemText
primary={primary}
primaryTypographyProps={{
color: isActive ? 'primary' : 'inherit',
fontWeight: isActive ? 'bold' : 'regular',
}}
/>
</ListItemButton>
</ListItem>
);
};
const ResponsiveSidebar = () => {
const [mobileOpen, setMobileOpen] = useState(false);
const [openSubmenu, setOpenSubmenu] = useState(false);
const theme = useTheme();
const isLargeScreen = useMediaQuery(theme.breakpoints.up('md'));
const location = useLocation();
// Check if we're in the users section to auto-expand the submenu
React.useEffect(() => {
if (location.pathname.includes('/users')) {
setOpenSubmenu(true);
}
}, [location.pathname]);
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
const handleSubmenuToggle = () => {
setOpenSubmenu(!openSubmenu);
};
// Close mobile drawer when a link is clicked
const handleNavClick = () => {
if (!isLargeScreen) {
setMobileOpen(false);
}
};
const drawerContent = (
<>
<Toolbar>
<Typography variant="h6" noWrap component="div">
My App
</Typography>
</Toolbar>
<Divider />
<List>
<NavItem
to="/"
icon={<HomeIcon />}
primary="Dashboard"
onClick={handleNavClick}
/>
<NavItem
to="/messages"
icon={<MailIcon />}
primary="Messages"
onClick={handleNavClick}
/>
{/* Nested navigation item */}
<ListItem disablePadding>
<ListItemButton onClick={handleSubmenuToggle}>
<ListItemIcon>
<PeopleIcon />
</ListItemIcon>
<ListItemText primary="Users" />
{openSubmenu ? <ExpandLess /> : <ExpandMore />}
</ListItemButton>
</ListItem>
<Collapse in={openSubmenu} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
<NavItem
to="/users/profiles"
icon={<PersonIcon />}
primary="User Profiles"
onClick={handleNavClick}
nested
/>
<NavItem
to="/users/groups"
icon={<GroupIcon />}
primary="User Groups"
onClick={handleNavClick}
nested
/>
</List>
</Collapse>
<NavItem
to="/settings"
icon={<SettingsIcon />}
primary="Settings"
onClick={handleNavClick}
/>
</List>
</>
);
return (
<Box sx={{ display: 'flex' }}>
<AppBar
position="fixed"
sx={{
width: { md: `calc(100% - ${drawerWidth}px)` },
ml: { md: `${drawerWidth}px` },
}}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { md: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div">
{/* Display current page title based on route */}
{location.pathname === '/' && 'Dashboard'}
{location.pathname === '/messages' && 'Messages'}
{location.pathname === '/users/profiles' && 'User Profiles'}
{location.pathname === '/users/groups' && 'User Groups'}
{location.pathname === '/settings' && 'Settings'}
</Typography>
</Toolbar>
</AppBar>
<Box
component="nav"
sx={{ width: { md: drawerWidth }, flexShrink: { md: 0 } }}
>
<Drawer
variant="temporary"
open={mobileOpen}
onClose={handleDrawerToggle}
ModalProps={{
keepMounted: true,
}}
sx={{
display: { xs: 'block', md: 'none' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
>
{drawerContent}
</Drawer>
<Drawer
variant="permanent"
sx={{
display: { xs: 'none', md: 'block' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
open
>
{drawerContent}
</Drawer>
</Box>
<Box
component="main"
sx={{
flexGrow: 1,
p: 3,
width: { md: `calc(100% - ${drawerWidth}px)` },
marginTop: '64px',
}}
>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/messages" element={<Messages />} />
<Route path="/users/profiles" element={<UserProfiles />} />
<Route path="/users/groups" element={<UserGroups />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Box>
</Box>
);
};
// Wrapper component to provide routing context
const App = () => {
return (
<BrowserRouter>
<ResponsiveSidebar />
</BrowserRouter>
);
};
export default App;
This implementation adds several important features:
- Active state highlighting - The current route is highlighted in the sidebar
- Auto-expanding submenus - If you navigate to a route within a submenu, that submenu automatically expands
- Mobile UX improvement - The drawer automatically closes after selecting a link on mobile
- Dynamic page title - The AppBar title changes based on the current route
- Route configuration - Proper routing with React Router's components
Customizing the Drawer's Appearance
Now that we have a fully functional responsive drawer with routing, let's explore how to customize its appearance using MUI's styling system.
Styling with the sx Prop
The sx
prop is MUI's primary way to apply styles directly to components. Let's use it to customize our drawer:
// Inside your component, update the Drawer styling:
// For the permanent drawer
<Drawer
variant="permanent"
sx={{
display: { xs: 'none', md: 'block' },
'& .MuiDrawer-paper': {
boxSizing: 'border-box',
width: drawerWidth,
borderRight: '1px solid rgba(0, 0, 0, 0.12)',
backgroundColor: 'background.paper',
boxShadow: 1,
},
}}
open
>
{drawerContent}
</Drawer>
// For the temporary drawer
<Drawer
variant="temporary"
open={mobileOpen}
onClose={handleDrawerToggle}
ModalProps={{
keepMounted: true,
}}
sx={{
display: { xs: 'block', md: 'none' },
'& .MuiDrawer-paper': {
boxSizing: 'border-box',
width: drawerWidth,
backgroundColor: 'background.paper',
boxShadow: 3,
},
}}
>
{drawerContent}
</Drawer>
Customizing with Theme Overrides
For more global customization, you can override the Drawer component in your theme:
import { createTheme, ThemeProvider } from '@mui/material/styles';
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import ResponsiveSidebar from './ResponsiveSidebar';
import { CssBaseline } from '@mui/material';
const theme = createTheme({
components: {
MuiDrawer: {
styleOverrides: {
paper: {
backgroundColor: '#f5f5f5',
color: '#333',
'& .MuiListItemButton-root': {
borderRadius: 8,
margin: '4px 8px',
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.04)',
},
},
'& .MuiListItemIcon-root': {
minWidth: 40,
},
},
},
},
},
palette: {
primary: {
main: '#1976d2',
},
secondary: {
main: '#dc004e',
},
background: {
default: '#fff',
paper: '#f5f5f5',
},
},
});
function App() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<BrowserRouter>
<ResponsiveSidebar />
</BrowserRouter>
</ThemeProvider>
);
}
export default App;
Creating a Custom Styled Drawer
For even more control, you can use the styled
API to create custom styled components:
import { styled } from '@mui/material/styles';
import { Drawer as MuiDrawer } from '@mui/material';
const drawerWidth = 240;
// Custom styled drawer for permanent variant
const PermanentDrawer = styled(MuiDrawer, {
shouldForwardProp: (prop) => prop !== 'open' && prop !== 'drawerWidth'
})(({ theme, open, drawerWidth }) => ({
width: drawerWidth,
flexShrink: 0,
whiteSpace: 'nowrap',
boxSizing: 'border-box',
display: { xs: 'none', md: 'block' },
'& .MuiDrawer-paper': {
width: drawerWidth,
boxSizing: 'border-box',
borderRight: '1px solid rgba(0, 0, 0, 0.12)',
backgroundColor: theme.palette.background.paper,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
overflowX: 'hidden',
boxShadow: theme.shadows[1],
},
}));
// Then in your component:
<PermanentDrawer
variant="permanent"
open={true}
drawerWidth={drawerWidth}
>
{drawerContent}
</PermanentDrawer>
Implementing a Mini Variant Drawer
A popular pattern is the "mini variant" drawer, which collapses to show only icons when not fully expanded. Let's implement this feature:
import React, { useState } from 'react';
import { BrowserRouter, Routes, Route, Link, useLocation } from 'react-router-dom';
import {
Box,
Drawer,
AppBar,
Toolbar,
List,
Typography,
Divider,
IconButton,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
useTheme,
useMediaQuery,
Tooltip,
} from '@mui/material';
import {
Menu as MenuIcon,
ChevronLeft as ChevronLeftIcon,
ChevronRight as ChevronRightIcon,
Home as HomeIcon,
Mail as MailIcon,
Settings as SettingsIcon,
People as PeopleIcon,
} from '@mui/icons-material';
import { styled } from '@mui/material/styles';
const drawerWidth = 240;
const miniDrawerWidth = 65;
// Styled components for the mini variant drawer
const openedMixin = (theme) => ({
width: drawerWidth,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
overflowX: 'hidden',
});
const closedMixin = (theme) => ({
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
overflowX: 'hidden',
width: `${miniDrawerWidth}px`,
});
const DrawerHeader = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
padding: theme.spacing(0, 1),
...theme.mixins.toolbar,
}));
const MiniDrawer = styled(Drawer, { shouldForwardProp: (prop) => prop !== 'open' })(
({ theme, open }) => ({
width: open ? drawerWidth : miniDrawerWidth,
flexShrink: 0,
whiteSpace: 'nowrap',
boxSizing: 'border-box',
...(open && {
...openedMixin(theme),
'& .MuiDrawer-paper': openedMixin(theme),
}),
...(!open && {
...closedMixin(theme),
'& .MuiDrawer-paper': closedMixin(theme),
}),
}),
);
// Individual page components
const Dashboard = () => <Typography variant="h4">Dashboard Page</Typography>;
const Messages = () => <Typography variant="h4">Messages Page</Typography>;
const Users = () => <Typography variant="h4">Users Page</Typography>;
const Settings = () => <Typography variant="h4">Settings Page</Typography>;
const MiniVariantDrawer = () => {
const [open, setOpen] = useState(true);
const [mobileOpen, setMobileOpen] = useState(false);
const theme = useTheme();
const isLargeScreen = useMediaQuery(theme.breakpoints.up('md'));
const location = useLocation();
const handleDrawerToggle = () => {
if (isLargeScreen) {
setOpen(!open);
} else {
setMobileOpen(!mobileOpen);
}
};
// Navigation items
const navItems = [
{ text: 'Dashboard', icon: <HomeIcon />, path: '/' },
{ text: 'Messages', icon: <MailIcon />, path: '/messages' },
{ text: 'Users', icon: <PeopleIcon />, path: '/users' },
{ text: 'Settings', icon: <SettingsIcon />, path: '/settings' },
];
const drawerContent = (
<>
<DrawerHeader>
{open && (
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1, ml: 2 }}>
My App
</Typography>
)}
<IconButton onClick={handleDrawerToggle}>
{open ? (
theme.direction === 'rtl' ? <ChevronRightIcon /> : <ChevronLeftIcon />
) : (
theme.direction === 'rtl' ? <ChevronLeftIcon /> : <ChevronRightIcon />
)}
</IconButton>
</DrawerHeader>
<Divider />
<List>
{navItems.map((item) => {
const isActive = location.pathname === item.path;
return (
<ListItem key={item.text} disablePadding sx={{ display: 'block' }}>
<Tooltip title={!open ? item.text : ''} placement="right">
<ListItemButton
component={Link}
to={item.path}
sx={{
minHeight: 48,
justifyContent: open ? 'initial' : 'center',
px: 2.5,
bgcolor: isActive ? 'rgba(0, 0, 0, 0.08)' : 'transparent',
'&:hover': {
bgcolor: isActive ? 'rgba(0, 0, 0, 0.12)' : 'rgba(0, 0, 0, 0.04)',
}
}}
>
<ListItemIcon
sx={{
minWidth: 0,
mr: open ? 3 : 'auto',
justifyContent: 'center',
color: isActive ? 'primary.main' : 'inherit',
}}
>
{item.icon}
</ListItemIcon>
<ListItemText
primary={item.text}
sx={{
opacity: open ? 1 : 0,
color: isActive ? 'primary.main' : 'inherit',
'& .MuiTypography-root': {
fontWeight: isActive ? 'bold' : 'regular',
}
}}
/>
</ListItemButton>
</Tooltip>
</ListItem>
);
})}
</List>
</>
);
return (
<Box sx={{ display: 'flex' }}>
<AppBar
position="fixed"
sx={{
width: {
xs: '100%',
md: open ? `calc(100% - ${drawerWidth}px)` : `calc(100% - ${miniDrawerWidth}px)`
},
ml: {
xs: 0,
md: open ? `${drawerWidth}px` : `${miniDrawerWidth}px`
},
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
}}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { md: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div">
{/* Display current page title based on route */}
{location.pathname === '/' && 'Dashboard'}
{location.pathname === '/messages' && 'Messages'}
{location.pathname === '/users' && 'Users'}
{location.pathname === '/settings' && 'Settings'}
</Typography>
</Toolbar>
</AppBar>
{/* Mobile drawer */}
<Drawer
variant="temporary"
open={mobileOpen}
onClose={handleDrawerToggle}
ModalProps={{
keepMounted: true,
}}
sx={{
display: { xs: 'block', md: 'none' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
>
{drawerContent}
</Drawer>
{/* Desktop mini variant drawer */}
<MiniDrawer
variant="permanent"
open={open}
sx={{
display: { xs: 'none', md: 'block' },
}}
>
{drawerContent}
</MiniDrawer>
<Box
component="main"
sx={{
flexGrow: 1,
p: 3,
width: {
xs: '100%',
md: open ? `calc(100% - ${drawerWidth}px)` : `calc(100% - ${miniDrawerWidth}px)`
},
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
marginTop: '64px',
}}
>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/messages" element={<Messages />} />
<Route path="/users" element={<Users />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Box>
</Box>
);
};
// Wrapper component to provide routing context
const App = () => {
return (
<BrowserRouter>
<MiniVariantDrawer />
</BrowserRouter>
);
};
export default App;
This mini variant drawer implementation includes several advanced features:
- Smooth transitions - The drawer smoothly transitions between expanded and collapsed states
- Tooltips for collapsed state - When collapsed, tooltips show the menu item names on hover
- Responsive behavior - Still maintains the mobile/desktop responsive pattern
- Active state highlighting - Maintains the active state highlighting from earlier examples
- Custom styled components - Uses MUI's styled API for custom components
Accessibility Considerations
Accessibility is crucial for any navigation system. Let's enhance our drawer to be more accessible:
// Add these accessibility enhancements to your drawer component
// For the toggle button in the AppBar
<IconButton
color="inherit"
aria-label="open navigation drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { md: 'none' } }}
>
<MenuIcon />
</IconButton>
// For the drawer itself
<Drawer
variant="temporary"
open={mobileOpen}
onClose={handleDrawerToggle}
ModalProps={{
keepMounted: true,
}}
sx={{
display: { xs: 'block', md: 'none' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
aria-label="Main navigation"
>
{drawerContent}
</Drawer>
// For navigation items
<ListItemButton
component={Link}
to={item.path}
aria-current={isActive ? 'page' : undefined}
sx={{
minHeight: 48,
justifyContent: open ? 'initial' : 'center',
px: 2.5,
bgcolor: isActive ? 'rgba(0, 0, 0, 0.08)' : 'transparent',
}}
>
<ListItemIcon
sx={{
minWidth: 0,
mr: open ? 3 : 'auto',
justifyContent: 'center',
}}
aria-hidden="true"
>
{item.icon}
</ListItemIcon>
<ListItemText primary={item.text} sx={{ opacity: open ? 1 : 0 }} />
</ListItemButton>
Key accessibility enhancements:
- Proper ARIA labels - Adding descriptive
aria-label
attributes to interactive elements - Current page indication - Using
aria-current="page"
to indicate the active page - Decorative elements - Marking icons as
aria-hidden="true"
since they're decorative - Keyboard navigation - MUI's components already support keyboard navigation, but it's important to maintain this support
Performance Optimization
For large applications with many navigation items, performance can become an issue. Here are some optimizations:
import React, { useState, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
// Inside your component:
// Memoize the drawer content to prevent unnecessary re-renders
const DrawerContent = React.memo(({ open, navItems, location, handleItemClick }) => {
return (
<List>
{navItems.map((item) => {
const isActive = location.pathname === item.path;
return (
<ListItem key={item.text} disablePadding sx={{ display: 'block' }}>
<ListItemButton
component={Link}
to={item.path}
onClick={handleItemClick}
aria-current={isActive ? 'page' : undefined}
sx={{
minHeight: 48,
justifyContent: open ? 'initial' : 'center',
px: 2.5,
bgcolor: isActive ? 'rgba(0, 0, 0, 0.08)' : 'transparent',
}}
>
<ListItemIcon
sx={{
minWidth: 0,
mr: open ? 3 : 'auto',
justifyContent: 'center',
}}
aria-hidden="true"
>
{item.icon}
</ListItemIcon>
<ListItemText primary={item.text} sx={{ opacity: open ? 1 : 0 }} />
</ListItemButton>
</ListItem>
);
})}
</List>
);
});
const ResponsiveDrawer = () => {
const [open, setOpen] = useState(true);
const [mobileOpen, setMobileOpen] = useState(false);
const location = useLocation();
// Memoize the navigation items to prevent recreating on each render
const navItems = useMemo(() => [
{ text: 'Dashboard', icon: <HomeIcon />, path: '/' },
{ text: 'Messages', icon: <MailIcon />, path: '/messages' },
{ text: 'Users', icon: <PeopleIcon />, path: '/users' },
{ text: 'Settings', icon: <SettingsIcon />, path: '/settings' },
], []);
// Handle navigation item click (closes mobile drawer)
const handleItemClick = React.useCallback(() => {
if (mobileOpen) {
setMobileOpen(false);
}
}, [mobileOpen]);
// Rest of your component...
return (
// ...
<DrawerContent
open={open}
navItems={navItems}
location={location}
handleItemClick={handleItemClick}
/>
// ...
);
};
Performance optimizations include:
- Memoization - Using
React.memo
to prevent unnecessary re-renders of the drawer content - useMemo for static data - Memoizing the navigation items array to prevent recreating it on each render
- useCallback for handlers - Using
useCallback
for event handlers to maintain referential equality
Common Issues and Their Solutions
Let's address some common issues developers face when implementing responsive drawers:
1. Content Shifting When Drawer Opens/Closes
// Problem: Content shifts when drawer opens/closes
// Solution: Use CSS transitions and fixed positioning
<Box
component="main"
sx={{
flexGrow: 1,
p: 3,
width: {
xs: '100%',
md: open ? `calc(100% - ${drawerWidth}px)` : `calc(100% - ${miniDrawerWidth}px)`
},
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
marginLeft: {
xs: 0,
md: open ? `${drawerWidth}px` : `${miniDrawerWidth}px`
},
marginTop: '64px',
}}
>
{/* Your content */}
</Box>
2. Z-Index Issues with Drawer and Other Elements
// Problem: Drawer appears behind other elements
// Solution: Adjust z-index values
<AppBar
position="fixed"
sx={{
zIndex: (theme) => theme.zIndex.drawer + 1, // Make AppBar appear above drawer
// Other styles...
}}
>
{/* AppBar content */}
</AppBar>
<Drawer
sx={{
zIndex: (theme) => theme.zIndex.drawer, // Ensure proper z-index
// Other styles...
}}
>
{/* Drawer content */}
</Drawer>
3. Drawer Content Scrolling Issues
// Problem: Long drawer content doesn't scroll properly
// Solution: Add proper overflow handling
<Drawer
sx={{
'& .MuiDrawer-paper': {
boxSizing: 'border-box',
width: drawerWidth,
overflowX: 'hidden', // Prevent horizontal scrolling
display: 'flex',
flexDirection: 'column', // Enable proper flex layout
},
}}
>
<Toolbar /> {/* Space for AppBar */}
<Box sx={{ overflow: 'auto', flexGrow: 1 }}> {/* Scrollable container */}
<List>
{/* Navigation items */}
</List>
</Box>
<Divider />
<Box sx={{ p: 2 }}> {/* Fixed footer content */}
<Typography variant="body2">v1.0.0</Typography>
</Box>
</Drawer>
4. Drawer Doesn't Close on Mobile After Navigation
// Problem: Drawer stays open on mobile after clicking a link
// Solution: Close drawer on navigation
const handleNavItemClick = () => {
if (!isLargeScreen) {
setMobileOpen(false);
}
};
// Then in your navigation items:
<ListItemButton
component={Link}
to={item.path}
onClick={handleNavItemClick}
// Other props...
>
{/* Button content */}
</ListItemButton>
Best Practices for MUI Drawer Implementation
After years of working with MUI's Drawer component, here are the best practices I've learned:
1. Follow a Responsive Pattern
Always implement your drawer with responsiveness in mind:
- Mobile: Use temporary drawer that can be toggled open/closed
- Tablet: Consider using a mini variant or persistent drawer
- Desktop: Use permanent or mini variant drawer
2. Keep Performance in Mind
- Memoize drawer content and navigation items
- Use
React.memo
for components that don't need to re-render often - Consider code splitting for large navigation structures
3. Maintain Accessibility
- Add proper ARIA attributes
- Ensure keyboard navigation works correctly
- Provide sufficient color contrast for text and icons
- Test with screen readers
4. Implement Proper User Experience
- Close mobile drawer after navigation
- Highlight the current page/section
- Provide visual feedback for hover/focus states
- Use transitions for smooth open/close animations
5. Structure Your Code Well
- Separate drawer logic from page content
- Create reusable components for navigation items
- Use consistent naming conventions
Wrapping Up
Building a responsive sidebar with MUI's Drawer component involves understanding the different drawer variants, implementing proper responsiveness, and paying attention to user experience details. By following the patterns in this guide, you can create a sidebar that works well across all devices and provides an excellent user experience.
Remember that the best drawer implementation is one that feels natural to your users and fits seamlessly with your application's design language. Don't be afraid to customize the drawer to match your specific needs, whether that means adding custom animations, creating unique styling, or implementing complex navigation patterns.
With the techniques covered in this guide, you should now have all the tools you need to implement a professional-grade responsive sidebar in your React application using Material UI.