Building a Dropdown User Menu with Avatars Using React MUI Menu
As a front-end developer working with React and Material UI, you'll often need to create user interface components that combine functionality with visual appeal. One common UI pattern is the dropdown user menu - a compact component that expands to reveal user-related actions when clicked. In this article, I'll walk you through building a professional dropdown user menu with avatars using MUI's Menu component.
What You'll Learn
By the end of this article, you'll know how to:
- Implement a dropdown user menu with proper menu positioning
- Integrate user avatars into menu items
- Handle menu opening and closing with proper state management
- Style menu items with custom theming
- Add keyboard navigation and accessibility features
- Implement advanced patterns like nested menus and dividers
- Optimize performance and handle edge cases
Understanding MUI's Menu Component
The Menu component in Material UI provides a temporary surface containing choices that appear when triggered by an anchor element. It's the perfect foundation for dropdown interfaces like navigation menus, settings panels, and user account menus.
Core Menu Concepts
The Menu component follows a specific pattern in MUI that's essential to understand before implementation:
- Anchor Element: The element (like a button) that triggers the menu to open
- Menu State: Boolean state to track if the menu is open or closed
- Position Management: Controls where the menu appears relative to the anchor
- Menu Items: Individual selectable options inside the menu
Let's explore the key props that make the Menu component work:
Prop | Type | Default | Description |
---|---|---|---|
open | boolean | false | Controls whether the menu is displayed |
anchorEl | HTMLElement | null | null | The DOM element used to position the menu |
onClose | function | - | Callback fired when the menu is closed |
children | node | - | Menu contents, usually MenuItem components |
anchorOrigin | object | vertical: 'top', horizontal: 'left' | Position of the menu relative to anchor |
transformOrigin | object | vertical: 'top', horizontal: 'left' | Position for the menu's transform origin |
autoFocus | boolean | true | If true, the menu will focus the first item when opened |
MenuListProps | object | Props applied to the MenuList element | |
elevation | number | 8 | Shadow depth of the menu paper |
Controlled vs. Uncontrolled Usage
The Menu component can be used in both controlled and uncontrolled patterns:
Controlled Menu: You manage the open/closed state explicitly with React state. This gives you complete control over when the menu appears and disappears.
Uncontrolled Menu: The component manages its own internal state. This is simpler but offers less control over the menu's behavior.
For most professional applications, I recommend using the controlled pattern as it provides more flexibility and predictability, especially when integrating with other state management systems.
Building the User Menu: Step-by-Step Guide
Let's build a professional dropdown user menu with an avatar. I'll break this down into manageable steps with explanations for each part of the implementation.
Step 1: Setting Up the Project
First, let's ensure we have the necessary dependencies installed:
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled
For a user menu with avatars, we'll need these specific components:
import React, { useState } from 'react';
import {
Avatar,
Button,
Divider,
IconButton,
ListItemIcon,
Menu,
MenuItem,
Tooltip,
Typography
} from '@mui/material';
import {
AccountCircle,
Logout,
PersonAdd,
Settings
} from '@mui/icons-material';
These imports give us the core components we need: Menu for the dropdown container, MenuItem for individual options, Avatar for the user image, and various icons for menu actions.
Step 2: Creating the Basic Avatar Button
First, we'll create the avatar button that will serve as our anchor element for the menu:
function UserMenu() {
// State to track whether menu is open
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
// Handler for opening the menu
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
// Handler for closing the menu
const handleClose = () => {
setAnchorEl(null);
};
return (
<React.Fragment>
<Tooltip title="Account settings">
<IconButton
onClick={handleClick}
size="small"
aria-controls={open ? 'account-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
>
<Avatar sx={{ width: 32, height: 32 }}>M</Avatar>
</IconButton>
</Tooltip>
{/* Menu component will go here */}
</React.Fragment>
);
}
In this code:
- We create a state variable
anchorEl
to track the DOM element that will anchor our menu - We derive the
open
state from the presence of an anchor element - We set up handlers for opening and closing the menu
- We create an IconButton with an Avatar inside it, setting proper ARIA attributes for accessibility
- We wrap it in a Tooltip to provide additional context on hover
The Avatar component is initialized with just a letter "M" as a placeholder, but it could easily display an image of the user or their initials.
Step 3: Adding the Menu Component
Now, let's add the Menu component that will display when the avatar is clicked:
function UserMenu() {
// Previous state and handlers remain the same...
return (
<React.Fragment>
{/* Avatar button from previous step */}
<Menu
anchorEl={anchorEl}
id="account-menu"
open={open}
onClose={handleClose}
onClick={handleClose}
PaperProps={{
elevation: 0,
sx: {
overflow: 'visible',
filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
mt: 1.5,
'& .MuiAvatar-root': {
width: 32,
height: 32,
ml: -0.5,
mr: 1,
},
'&:before': {
content: '""',
display: 'block',
position: 'absolute',
top: 0,
right: 14,
width: 10,
height: 10,
bgcolor: 'background.paper',
transform: 'translateY(-50%) rotate(45deg)',
zIndex: 0,
},
},
}}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
{/* Menu items will go here */}
</Menu>
</React.Fragment>
);
}
This code adds the Menu component with several important configurations:
- Anchor and State: We connect the menu to our avatar button using
anchorEl
and control its visibility with theopen
prop - Positioning: We use
transformOrigin
andanchorOrigin
to position the menu below and aligned to the right of the avatar - Styling: We use the
PaperProps
to style the menu with a custom drop shadow and add a small triangular pointer at the top - Behavior: We set up the menu to close when clicked or when the user clicks outside of it
The custom styling creates a more elegant, modern look than the default Material UI menu appearance.
Step 4: Adding Menu Items with Avatars and Icons
Now, let's populate our menu with items, including avatars and icons:
function UserMenu() {
// Previous state and handlers remain the same...
return (
<React.Fragment>
{/* Avatar button and Menu container from previous steps */}
<Menu
{/* Menu props from previous step */}
>
<MenuItem onClick={handleClose}>
<Avatar /> My Profile
</MenuItem>
<MenuItem onClick={handleClose}>
<Avatar /> My Account
</MenuItem>
<Divider />
<MenuItem onClick={handleClose}>
<ListItemIcon>
<PersonAdd fontSize="small" />
</ListItemIcon>
Add another account
</MenuItem>
<MenuItem onClick={handleClose}>
<ListItemIcon>
<Settings fontSize="small" />
</ListItemIcon>
Settings
</MenuItem>
<MenuItem onClick={handleClose}>
<ListItemIcon>
<Logout fontSize="small" />
</ListItemIcon>
Logout
</MenuItem>
</Menu>
</React.Fragment>
);
}
In this step, we've added:
- Menu items with avatars for profile-related actions
- A divider to separate different categories of actions
- Menu items with icons for settings and system actions
- Click handlers that close the menu when an option is selected
The ListItemIcon component is used to properly align icons within menu items, ensuring consistent spacing and alignment.
Step 5: Enhancing with User Data
Let's improve our menu by integrating actual user data:
function UserMenu({ user }) {
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
// Function to generate avatar content based on user data
const getAvatarContent = () => {
if (user?.photoUrl) {
return <Avatar src={user.photoUrl} alt={user.name} sx={{ width: 32, height: 32 }} />;
}
// If no photo, use initials
if (user?.name) {
const initials = user.name
.split(' ')
.map(part => part[0])
.join('')
.toUpperCase()
.substring(0, 2);
return <Avatar sx={{ width: 32, height: 32 }}>{initials}</Avatar>;
}
// Fallback
return <Avatar sx={{ width: 32, height: 32 }}><AccountCircle /></Avatar>;
};
return (
<React.Fragment>
<Tooltip title="Account settings">
<IconButton
onClick={handleClick}
size="small"
aria-controls={open ? 'account-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
>
{getAvatarContent()}
</IconButton>
</Tooltip>
<Menu
{/* Menu props from previous step */}
>
<MenuItem onClick={handleClose}>
{getAvatarContent()}
<Typography variant="body1" sx={{ ml: 1 }}>
{user?.name || 'My Profile'}
</Typography>
</MenuItem>
<MenuItem onClick={handleClose}>
<ListItemIcon>
<AccountCircle fontSize="small" />
</ListItemIcon>
{user?.email || 'My Account'}
</MenuItem>
{/* Other menu items remain the same */}
</Menu>
</React.Fragment>
);
}
In this enhancement:
- We accept a
user
prop containing user information - We create a
getAvatarContent
helper function that intelligently displays:- The user's photo if available
- The user's initials if they have a name but no photo
- A generic account icon as a fallback
- We display the user's name and email in the menu items if available
- We use Typography to ensure proper text styling
This approach makes the component more flexible and able to handle various user data scenarios.
Step 6: Adding Action Handlers
Let's make our menu functional by adding action handlers:
function UserMenu({ user, onLogout, onProfileClick, onSettingsClick }) {
// Previous state and avatar logic remains the same...
const handleProfileClick = () => {
handleClose();
if (onProfileClick) onProfileClick();
};
const handleSettingsClick = () => {
handleClose();
if (onSettingsClick) onSettingsClick();
};
const handleLogoutClick = () => {
handleClose();
if (onLogout) onLogout();
};
return (
<React.Fragment>
{/* Avatar button remains the same */}
<Menu
{/* Menu props remain the same */}
>
<MenuItem onClick={handleProfileClick}>
{getAvatarContent()}
<Typography variant="body1" sx={{ ml: 1 }}>
{user?.name || 'My Profile'}
</Typography>
</MenuItem>
{/* Other menu items with their respective handlers */}
<MenuItem onClick={handleSettingsClick}>
<ListItemIcon>
<Settings fontSize="small" />
</ListItemIcon>
Settings
</MenuItem>
<MenuItem onClick={handleLogoutClick}>
<ListItemIcon>
<Logout fontSize="small" />
</ListItemIcon>
Logout
</MenuItem>
</Menu>
</React.Fragment>
);
}
In this step, we:
- Accept callback props for different menu actions
- Create handler functions that close the menu and then call the appropriate callback
- Connect these handlers to the respective menu items
This pattern allows the parent component to control what happens when menu items are clicked, making our UserMenu component reusable in different contexts.
Step 7: Enhancing Accessibility
Let's improve the accessibility of our menu:
function UserMenu({ user, onLogout, onProfileClick, onSettingsClick }) {
// Previous state and handlers remain the same...
return (
<React.Fragment>
<Tooltip title="Account settings">
<IconButton
onClick={handleClick}
size="small"
aria-controls={open ? 'account-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
aria-label="User account menu"
>
{getAvatarContent()}
</IconButton>
</Tooltip>
<Menu
anchorEl={anchorEl}
id="account-menu"
open={open}
onClose={handleClose}
onClick={handleClose}
PaperProps={{ /* Same as before */ }}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
MenuListProps={{
'aria-labelledby': 'account-button',
dense: true,
}}
>
{/* Menu items remain the same */}
</Menu>
</React.Fragment>
);
}
In this accessibility enhancement:
- We add an explicit
aria-label
to the IconButton to describe its purpose - We use
MenuListProps
to setaria-labelledby
which connects the menu to its trigger button - We set
dense: true
to make the menu more compact and easier to navigate
These changes ensure that screen readers can properly announce the menu and its relationship to the button that opens it.
Step 8: Adding Animation and Transitions
Let's add smooth animations to our menu:
import { Fade } from '@mui/material';
function UserMenu({ user, onLogout, onProfileClick, onSettingsClick }) {
// Previous state and handlers remain the same...
return (
<React.Fragment>
{/* Avatar button remains the same */}
<Menu
anchorEl={anchorEl}
id="account-menu"
open={open}
onClose={handleClose}
onClick={handleClose}
PaperProps={{ /* Same as before */ }}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
MenuListProps={{
'aria-labelledby': 'account-button',
dense: true,
}}
TransitionComponent={Fade}
transitionDuration={250}
>
{/* Menu items remain the same */}
</Menu>
</React.Fragment>
);
}
In this step, we add:
- The
TransitionComponent
prop set toFade
for a smooth fade-in effect - A custom
transitionDuration
to control the speed of the animation
These subtle animations make the interface feel more polished and responsive without being distracting.
Step 9: Putting It All Together
Now, let's combine everything into a complete, reusable component:
import React, { useState } from 'react';
import {
Avatar,
Divider,
Fade,
IconButton,
ListItemIcon,
Menu,
MenuItem,
Tooltip,
Typography
} from '@mui/material';
import {
AccountCircle,
Logout,
PersonAdd,
Settings
} from '@mui/icons-material';
function UserMenu({
user,
onLogout,
onProfileClick,
onSettingsClick,
onAddAccountClick
}) {
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
// Generate avatar content based on user data
const getAvatarContent = () => {
if (user?.photoUrl) {
return <Avatar src={user.photoUrl} alt={user.name} sx={{ width: 32, height: 32 }} />;
}
if (user?.name) {
const initials = user.name
.split(' ')
.map(part => part[0])
.join('')
.toUpperCase()
.substring(0, 2);
return <Avatar sx={{ width: 32, height: 32 }}>{initials}</Avatar>;
}
return <Avatar sx={{ width: 32, height: 32 }}><AccountCircle /></Avatar>;
};
// Action handlers
const handleProfileClick = () => {
handleClose();
if (onProfileClick) onProfileClick();
};
const handleSettingsClick = () => {
handleClose();
if (onSettingsClick) onSettingsClick();
};
const handleAddAccountClick = () => {
handleClose();
if (onAddAccountClick) onAddAccountClick();
};
const handleLogoutClick = () => {
handleClose();
if (onLogout) onLogout();
};
return (
<React.Fragment>
<Tooltip title="Account settings">
<IconButton
onClick={handleClick}
size="small"
aria-controls={open ? 'account-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
aria-label="User account menu" >
{getAvatarContent()}
</IconButton>
</Tooltip>
<Menu
anchorEl={anchorEl}
id="account-menu"
open={open}
onClose={handleClose}
PaperProps={{
elevation: 0,
sx: {
overflow: 'visible',
filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
mt: 1.5,
'& .MuiAvatar-root': {
width: 32,
height: 32,
ml: -0.5,
mr: 1,
},
'&:before': {
content: '""',
display: 'block',
position: 'absolute',
top: 0,
right: 14,
width: 10,
height: 10,
bgcolor: 'background.paper',
transform: 'translateY(-50%) rotate(45deg)',
zIndex: 0,
},
},
}}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
MenuListProps={{
'aria-labelledby': 'account-button',
dense: true,
}}
TransitionComponent={Fade}
transitionDuration={250}
>
<MenuItem onClick={handleProfileClick}>
{getAvatarContent()}
<Typography variant="body1" sx={{ ml: 1 }}>
{user?.name || 'My Profile'}
</Typography>
</MenuItem>
<MenuItem onClick={handleClose}>
<ListItemIcon>
<AccountCircle fontSize="small" />
</ListItemIcon>
{user?.email || 'My Account'}
</MenuItem>
<Divider />
<MenuItem onClick={handleAddAccountClick}>
<ListItemIcon>
<PersonAdd fontSize="small" />
</ListItemIcon>
Add another account
</MenuItem>
<MenuItem onClick={handleSettingsClick}>
<ListItemIcon>
<Settings fontSize="small" />
</ListItemIcon>
Settings
</MenuItem>
<MenuItem onClick={handleLogoutClick}>
<ListItemIcon>
<Logout fontSize="small" />
</ListItemIcon>
Logout
</MenuItem>
</Menu>
</React.Fragment>
);
}
export default UserMenu;
This complete component:
- Accepts user data and callback functions as props
- Intelligently displays user information when available
- Provides a clean, professional dropdown menu with avatars and icons
- Includes proper accessibility attributes
- Features smooth animations
- Handles all the state management internally
Advanced Customization Techniques
Let's explore some advanced customization options for our UserMenu component.
Theming the Menu
You can customize the appearance of the menu using MUI's theming system:
import { createTheme, ThemeProvider } from '@mui/material/styles';
// Create a custom theme for the menu
const menuTheme = createTheme({
components: {
MuiMenu: {
styleOverrides: {
paper: {
borderRadius: 8,
minWidth: 220,
},
},
},
MuiMenuItem: {
styleOverrides: {
root: {
padding: '10px 16px',
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.04)',
},
},
},
},
MuiAvatar: {
styleOverrides: {
root: {
backgroundColor: '#1976d2',
},
},
},
},
});
// Usage
function ThemedUserMenu(props) {
return (
<ThemeProvider theme={menuTheme}>
<UserMenu {...props} />
</ThemeProvider>
);
}
This theming approach allows you to:
- Customize the border radius of the menu
- Set a consistent minimum width
- Adjust padding and hover effects for menu items
- Set a default background color for avatars
Using Custom Styled Components
For more specific styling needs, you can use the styled
API:
import { styled } from '@mui/material/styles';
import { Menu, MenuItem, Avatar } from '@mui/material';
// Create styled versions of MUI components
const StyledMenu = styled(Menu)(({ theme }) => ({
'& .MuiPaper-root': {
borderRadius: 8,
boxShadow: '0px 5px 15px rgba(0, 0, 0, 0.15)',
marginTop: theme.spacing(1.5),
},
}));
const StyledMenuItem = styled(MenuItem)(({ theme }) => ({
padding: theme.spacing(1.5, 2),
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
}));
const UserAvatar = styled(Avatar)(({ theme }) => ({
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
fontSize: 14,
width: 32,
height: 32,
}));
// Then use these components in your UserMenu
function EnhancedUserMenu(props) {
// ... state and handlers
return (
<React.Fragment>
{/* ... */}
<StyledMenu
// ... other props >
<StyledMenuItem onClick={handleProfileClick}>
<UserAvatar>{/* ... */}</UserAvatar>
{/* ... */}
</StyledMenuItem>
{/* ... other menu items */}
</StyledMenu>
</React.Fragment>
);
}
This approach gives you even more control over the styling of each component, allowing for consistent theming that respects your application's design system.
Adding a Responsive Design
Let's make our menu responsive to different screen sizes:
import { useMediaQuery, useTheme } from '@mui/material';
function ResponsiveUserMenu(props) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
// ... state and handlers
return (
<React.Fragment>
<Tooltip title="Account settings">
<IconButton
// ... other props
size={isMobile ? "medium" : "small"} >
<Avatar
sx={{
width: isMobile ? 40 : 32,
height: isMobile ? 40 : 32
}} >
{/* ... */}
</Avatar>
</IconButton>
</Tooltip>
<Menu
// ... other props
PaperProps={{
// ... other props
sx: {
// ... other styles
width: isMobile ? '100%' : 'auto',
maxWidth: isMobile ? '100%' : 320,
left: isMobile ? 0 : 'auto',
right: isMobile ? 0 : 'auto',
// ... other styles
},
}}
// Adjust positioning for mobile
anchorOrigin={isMobile
? { horizontal: 'center', vertical: 'bottom' }
: { horizontal: 'right', vertical: 'bottom' }
}
transformOrigin={isMobile
? { horizontal: 'center', vertical: 'top' }
: { horizontal: 'right', vertical: 'top' }
}
>
{/* ... menu items */}
</Menu>
</React.Fragment>
);
}
This responsive approach:
- Uses MUI's
useMediaQuery
hook to detect mobile screens - Adjusts the size of the avatar button for better touch targets on mobile
- Makes the menu full-width on mobile devices
- Centers the menu on mobile for better visibility
Performance Optimization
Let's optimize our UserMenu component for better performance:
Memoizing the Component
import React, { useState, useCallback, memo } from 'react';
// Memoize the component to prevent unnecessary re-renders
const UserMenu = memo(function UserMenu({
user,
onLogout,
onProfileClick,
onSettingsClick
}) {
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
// Memoize handlers to prevent recreating them on each render
const handleClick = useCallback((event) => {
setAnchorEl(event.currentTarget);
}, []);
const handleClose = useCallback(() => {
setAnchorEl(null);
}, []);
const handleProfileClick = useCallback(() => {
handleClose();
if (onProfileClick) onProfileClick();
}, [handleClose, onProfileClick]);
// ... other memoized handlers
// Memoize the avatar content generation
const avatarContent = useMemo(() => {
// ... avatar content generation logic
return <Avatar>{/* ... */}</Avatar>;
}, [user?.photoUrl, user?.name]);
return (
// ... component JSX
);
});
These optimizations:
- Memoize the entire component to prevent re-rendering when parent components render
- Use
useCallback
to memoize event handlers - Use
useMemo
for complex calculations like avatar content generation
Lazy Loading Menu Items
For menus with many items or complex content, you can implement lazy loading:
import React, { useState, lazy, Suspense } from 'react';
import { CircularProgress } from '@mui/material';
// Lazy load complex menu item components
const ProfileSection = lazy(() => import('./ProfileSection'));
const SettingsSection = lazy(() => import('./SettingsSection'));
function OptimizedUserMenu(props) {
// ... state and handlers
return (
<React.Fragment>
{/* ... button */}
<Menu
// ... menu props
>
<Suspense fallback={<MenuItem><CircularProgress size={20} /></MenuItem>}>
<ProfileSection user={props.user} onProfileClick={props.onProfileClick} />
</Suspense>
<Divider />
<Suspense fallback={<MenuItem><CircularProgress size={20} /></MenuItem>}>
<SettingsSection onSettingsClick={props.onSettingsClick} />
</Suspense>
{/* ... other items */}
</Menu>
</React.Fragment>
);
}
This approach is especially useful for complex menus with nested components or data fetching requirements.
Best Practices and Common Issues
Best Practices
-
Keep Menu Items Focused: Include only relevant actions in the user menu. Too many options can overwhelm users.
-
Use Consistent Iconography: Maintain visual consistency by using icons from the same family and with similar visual weight.
-
Group Related Actions: Use dividers to group related actions together, making the menu more scannable.
-
Provide Visual Feedback: Ensure menu items have clear hover and active states to indicate interactivity.
-
Mind the Mobile Experience: Ensure touch targets are large enough (at least 48px tall) on mobile devices.
-
Test Keyboard Navigation: Verify that users can navigate the menu using keyboard controls (Tab, Enter, Escape, Arrow keys).
Common Issues and Solutions
Issue: Menu Positioning Problems
Problem: Menu appears in unexpected positions, especially in containers with overflow: hidden
.
Solution:
<Menu
// ... other props
PopoverProps={{
disableScrollLock: true,
anchorReference: "anchorPosition",
anchorPosition: {
top: anchorRect?.bottom || 0,
left: anchorRect?.right || 0
},
}}
>
{/* ... */}
</Menu>
Issue: Menu Closes Unexpectedly
Problem: Menu closes when clicking on elements inside menu items.
Solution:
<MenuItem
onClick={(e) => {
// Prevent the event from bubbling up to the Menu's onClick handler
e.stopPropagation();
handleSomeAction();
}}
>
{/* ... */}
</MenuItem>
Issue: Focus Management
Problem: Focus is lost when menu closes, creating accessibility issues.
Solution:
function UserMenu() {
const buttonRef = useRef(null);
const handleClose = () => {
setAnchorEl(null);
// Return focus to the button when the menu closes
buttonRef.current?.focus();
};
return (
<React.Fragment>
<IconButton
ref={buttonRef}
// ... other props
>
{/* ... */}
</IconButton>
<Menu
// ... other props
onClose={handleClose}
>
{/* ... */}
</Menu>
</React.Fragment>
);
}
Issue: Scrolling Issues on Mobile
Problem: Opening the menu causes unexpected page scrolling on mobile devices.
Solution:
<Menu
// ... other props
disableScrollLock={false}
PopoverProps={{
disableRestoreFocus: true,
}}
>
{/* ... */}
</Menu>
Advanced Features
Adding Badge Notifications
Let's enhance our user menu with notification badges:
import { Badge } from '@mui/material';
function UserMenuWithNotifications({ user, notificationCount, ...props }) {
// ... state and handlers
return (
<React.Fragment>
<Tooltip title="Account settings">
<IconButton>
<Badge
badgeContent={notificationCount}
color="error"
overlap="circular"
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}} >
{getAvatarContent()}
</Badge>
</IconButton>
</Tooltip>
<Menu
// ... menu props
>
{/* ... menu items */}
{notificationCount > 0 && (
<MenuItem onClick={props.onViewNotifications}>
<ListItemIcon>
<Badge badgeContent={notificationCount} color="error">
<NotificationsIcon fontSize="small" />
</Badge>
</ListItemIcon>
View notifications
</MenuItem>
)}
{/* ... other menu items */}
</Menu>
</React.Fragment>
);
}
This enhancement:
- Adds a notification badge to the avatar button
- Conditionally adds a notification menu item when notifications are present
- Uses the same badge styling for consistency
Adding Nested Submenus
For complex user menus, you might want to add nested submenus:
import React, { useState } from 'react';
import {
// ... other imports
ArrowRight as ArrowRightIcon
} from '@mui/icons-material';
function UserMenuWithSubmenus(props) {
const [anchorEl, setAnchorEl] = useState(null);
const [settingsAnchorEl, setSettingsAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const settingsOpen = Boolean(settingsAnchorEl);
// ... other handlers
const handleSettingsClick = (event) => {
// Prevent main menu from closing
event.stopPropagation();
setSettingsAnchorEl(event.currentTarget);
};
const handleSettingsClose = () => {
setSettingsAnchorEl(null);
};
return (
<React.Fragment>
{/* Main button */}
<Tooltip title="Account settings">
<IconButton
onClick={handleClick}
// ... other props >
{getAvatarContent()}
</IconButton>
</Tooltip>
{/* Main menu */}
<Menu
// ... main menu props
>
{/* ... other menu items */}
<MenuItem
onClick={handleSettingsClick}
sx={{
'&.MuiMenuItem-root': {
justifyContent: 'space-between',
}
}}
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<ListItemIcon>
<Settings fontSize="small" />
</ListItemIcon>
Settings
</Box>
<ArrowRightIcon fontSize="small" sx={{ ml: 1 }} />
</MenuItem>
{/* ... other menu items */}
</Menu>
{/* Settings submenu */}
<Menu
anchorEl={settingsAnchorEl}
open={settingsOpen}
onClose={handleSettingsClose}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<MenuItem onClick={() => { handleSettingsClose(); props.onAppearanceSettings(); }}>
Appearance
</MenuItem>
<MenuItem onClick={() => { handleSettingsClose(); props.onPrivacySettings(); }}>
Privacy & Security
</MenuItem>
<MenuItem onClick={() => { handleSettingsClose(); props.onNotificationSettings(); }}>
Notifications
</MenuItem>
</Menu>
</React.Fragment>
);
}
This implementation:
- Manages separate state for the main menu and submenu
- Positions the submenu to appear to the right of its parent item
- Prevents event propagation to keep the main menu open when opening the submenu
- Adds a visual indicator (arrow icon) to show that an item has a submenu
Real-World Integration Example
Let's see how our UserMenu component might be integrated into a real application:
import React, { useContext } from 'react';
import { AppBar, Toolbar, Typography, Box } from '@mui/material';
import { useNavigate } from 'react-router-dom';
import UserMenu from './UserMenu';
import { AuthContext } from '../contexts/AuthContext';
import { NotificationContext } from '../contexts/NotificationContext';
function AppHeader() {
const { user, logout } = useContext(AuthContext);
const { notifications } = useContext(NotificationContext);
const navigate = useNavigate();
const handleProfileClick = () => {
navigate('/profile');
};
const handleSettingsClick = () => {
navigate('/settings');
};
const handleAddAccountClick = () => {
navigate('/add-account');
};
const handleLogout = async () => {
try {
await logout();
navigate('/login');
} catch (error) {
console.error('Logout failed:', error);
}
};
return (
<AppBar position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
My Application
</Typography>
{user ? (
<UserMenu
user={user}
notificationCount={notifications.length}
onProfileClick={handleProfileClick}
onSettingsClick={handleSettingsClick}
onAddAccountClick={handleAddAccountClick}
onLogout={handleLogout}
/>
) : (
<Box>
<Button color="inherit" onClick={() => navigate('/login')}>
Login
</Button>
<Button color="inherit" onClick={() => navigate('/register')}>
Register
</Button>
</Box>
)}
</Toolbar>
</AppBar>
);
}
export default AppHeader;
This integration example:
- Uses React context to access user and notification data
- Uses React Router for navigation
- Handles authentication state to show either the user menu or login/register buttons
- Implements navigation and logout functionality
Wrapping Up
Building a dropdown user menu with avatars using MUI's Menu component is a practical skill for any React developer. We've covered everything from basic implementation to advanced customization, performance optimization, and real-world integration.
The approach we've taken leverages MUI's component system while adding our own enhancements for a polished user experience. By following the patterns in this guide, you can create user menus that are not only visually appealing but also accessible, performant, and maintainable.
Remember that a well-designed user menu is an important part of your application's user experience. It should be intuitive, responsive, and aligned with your overall design system. With the techniques covered in this article, you now have the knowledge to create professional user menus that enhance your React applications.