How to Use React MUI Avatar to Build a User Profile List with Dynamic Images
Working with user interfaces often requires displaying user profiles in a clean, consistent way. Material-UI's Avatar component offers a powerful, flexible solution for this common need. In this guide, I'll walk you through building a comprehensive user profile list with dynamic images using MUI Avatar, covering everything from basic implementation to advanced customization techniques.
What You'll Learn
By the end of this article, you'll understand how to:
- Implement MUI Avatar with different content types (images, letters, icons)
- Build a responsive user profile list with dynamic data
- Handle image loading errors gracefully
- Customize Avatar appearance with theming and the sx prop
- Implement advanced features like grouped avatars and badges
- Optimize performance with proper React patterns
Understanding MUI Avatar Component
The Avatar component in Material-UI serves as a graphical representation of a user, displaying either an image, initials, or an icon. It's a fundamental building block for user interfaces that require visual identification.
Core Avatar Props and Features
The Avatar component accepts various props that control its appearance and behavior. Here's a comprehensive breakdown of the most important ones:
Prop | Type | Default | Description |
---|---|---|---|
alt | string | undefined | Alternative text for the avatar image (important for accessibility) |
children | node | undefined | Used for text or icon content when no image is available |
component | elementType | 'div' | The component used for the root node |
imgProps | object | Props applied to the img element if the component is used to display an image | |
sizes | string | undefined | The 'sizes' attribute for the img element |
src | string | undefined | The source of the image |
srcSet | string | undefined | The 'srcSet' attribute for the img element |
variant | 'circular' | 'rounded' | 'square' | 'circular' | The shape of the avatar |
sx | object | The system prop that allows defining custom styles |
The Avatar component is remarkably versatile. It can display three types of content:
- Images: When you provide a
src
prop, Avatar displays the image - Letters: When no image is available, it can show text (typically initials)
- Icons: You can also use Material icons as fallbacks
Avatar Variants and Sizes
MUI Avatar comes with three shape variants:
circular
(default): Perfect for profile picturesrounded
: Provides softened cornerssquare
: Creates a square avatar
While Avatar doesn't have built-in size variants, you can easily control its dimensions using the sx
prop or custom styles:
<Avatar sx={{ width: 24, height: 24 }}>S</Avatar>
<Avatar sx={{ width: 32, height: 32 }}>M</Avatar>
<Avatar sx={{ width: 56, height: 56 }}>L</Avatar>
Fallback Mechanism
One of Avatar's most useful features is its built-in fallback system. When an image fails to load, it automatically displays the provided children (text or icon). This ensures your UI remains intact even when image sources are unavailable.
<Avatar
alt="Remy Sharp"
src="/broken-image.jpg"
>
RS
</Avatar>
Accessibility Considerations
For proper accessibility, always provide an alt
attribute when using image avatars. This helps screen readers describe the avatar to visually impaired users. For letter avatars, the text content is automatically used as an accessible name.
Setting Up Your Project
Before diving into implementation, let's set up a React project with Material-UI installed. I'll guide you through each step to ensure you have everything configured properly.
Creating a New React Project
If you don't have a project yet, create one using Create React App:
npx create-react-app mui-avatar-profile-list
cd mui-avatar-profile-list
Installing Material-UI Dependencies
Next, install the required Material-UI packages:
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled
This installs the core MUI components, icons, and the Emotion styling engine that MUI uses under the hood.
Setting Up the Project Structure
Create a clean structure for your components:
mkdir -p src/components/UserProfileList
touch src/components/UserProfileList/UserProfileList.jsx
touch src/components/UserProfileList/UserAvatar.jsx
touch src/components/UserProfileList/mockData.js
Creating Mock Data
Let's create some realistic user data to work with. In mockData.js
, add:
export const users = [
{
id: 1,
name: "John Doe",
email: "john.doe@example.com",
avatar: "https://randomuser.me/api/portraits/men/1.jpg",
status: "active",
role: "Developer"
},
{
id: 2,
name: "Jane Smith",
email: "jane.smith@example.com",
avatar: "https://randomuser.me/api/portraits/women/2.jpg",
status: "away",
role: "Designer"
},
{
id: 3,
name: "Robert Johnson",
email: "robert.johnson@example.com",
avatar: "https://randomuser.me/api/portraits/men/3.jpg",
status: "offline",
role: "Manager"
},
{
id: 4,
name: "Emily Davis",
email: "emily.davis@example.com",
avatar: "https://broken-link.jpg",
status: "active",
role: "Product Owner"
},
{
id: 5,
name: "Michael Wilson",
email: "michael.wilson@example.com",
avatar: "https://randomuser.me/api/portraits/men/5.jpg",
status: "busy",
role: "QA Engineer"
}
];
Notice that I've intentionally included a broken image link for the fourth user to demonstrate the fallback mechanism.
Building a Basic User Avatar Component
Let's start by creating a reusable UserAvatar component that handles both successful image loading and fallbacks gracefully.
Creating the UserAvatar Component
In UserAvatar.jsx
, we'll build a component that:
- Displays the user's image when available
- Falls back to initials when the image fails to load
- Adds visual indicators for user status
import React from 'react';
import { Avatar, Badge, styled } from '@mui/material';
// Get initials from a name (e.g., "John Doe" -> "JD")
const getInitials = (name) => {
if (!name) return '?';
return name
.split(' ')
.map(word => word[0])
.join('')
.toUpperCase()
.substring(0, 2);
};
// Custom styled badge for status indicators
const StyledBadge = styled(Badge)(({ theme, status }) => ({
'& .MuiBadge-badge': {
backgroundColor:
status === 'active' ? '#44b700' :
status === 'away' ? '#ff9800' :
status === 'busy' ? '#f44336' :
'#bdbdbd',
color:
status === 'active' ? '#44b700' :
status === 'away' ? '#ff9800' :
status === 'busy' ? '#f44336' :
'#bdbdbd',
boxShadow: `0 0 0 2px ${theme.palette.background.paper}`,
'&::after': {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
borderRadius: '50%',
animation: status === 'active' ? 'ripple 1.2s infinite ease-in-out' : 'none',
border: '1px solid currentColor',
content: '""',
},
},
'@keyframes ripple': {
'0%': {
transform: 'scale(.8)',
opacity: 1,
},
'100%': {
transform: 'scale(2.4)',
opacity: 0,
},
},
}));
const UserAvatar = ({ user, size = 40, withStatus = true }) => {
const { name, avatar, status } = user;
const avatarContent = (
<Avatar
alt={name}
src={avatar}
sx={{
width: size,
height: size,
bgcolor: avatar ? 'transparent' : stringToColor(name),
fontSize: size * 0.4
}}
>
{getInitials(name)}
</Avatar>
);
// Generate a consistent color based on the name
function stringToColor(string) {
if (!string) return '#757575';
let hash = 0;
for (let i = 0; i < string.length; i++) {
hash = string.charCodeAt(i) + ((hash << 5) - hash);
}
let color = '#';
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
color += `00${value.toString(16)}`.slice(-2);
}
return color;
}
// If status indicator is requested and status is available
if (withStatus && status) {
return (
<StyledBadge
overlap="circular"
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
variant="dot"
status={status}
>
{avatarContent}
</StyledBadge>
);
}
return avatarContent;
};
export default UserAvatar;
This component includes several advanced features:
- Intelligent Fallback: If the image fails to load, it displays the user's initials
- Dynamic Color Generation: For letter avatars, it generates a consistent color based on the user's name
- Status Indicator: Optionally shows the user's status with a colored badge
- Customizable Size: Allows setting the avatar size through props
Building the User Profile List Component
Now, let's create the main component that will display a list of user profiles:
import React from 'react';
import {
List,
ListItem,
ListItemAvatar,
ListItemText,
ListItemSecondaryAction,
Typography,
Divider,
IconButton,
Chip,
Paper
} from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import UserAvatar from './UserAvatar';
import { users } from './mockData';
const UserProfileList = () => {
return (
<Paper elevation={2} sx={{ maxWidth: 600, mx: 'auto', mt: 4 }}>
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
{users.map((user, index) => (
<React.Fragment key={user.id}>
<ListItem alignItems="flex-start">
<ListItemAvatar>
<UserAvatar user={user} />
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="subtitle1" component="span">
{user.name}
</Typography>
}
secondary={
<>
<Typography
component="span"
variant="body2"
color="text.primary"
sx={{ display: 'block' }}
>
{user.email}
</Typography>
<Chip
label={user.role}
size="small"
sx={{ mt: 0.5 }}
/>
</>
}
/>
<ListItemSecondaryAction>
<IconButton edge="end">
<MoreVertIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
{index < users.length - 1 && <Divider variant="inset" component="li" />}
</React.Fragment>
))}
</List>
</Paper>
);
};
export default UserProfileList;
This component renders a clean list of user profiles with:
- User avatar with status indicator
- User name and email
- Role chip
- Action button for each user
- Dividers between users for better visual separation
Integrating in App.js
Now, let's integrate our component into the main App:
import React from 'react';
import { ThemeProvider, createTheme, CssBaseline, Container, Typography, Box } from '@mui/material';
import UserProfileList from './components/UserProfileList/UserProfileList';
// Create a theme instance
const theme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#1976d2',
},
secondary: {
main: '#dc004e',
},
},
});
function App() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Container>
<Box sx={{ my: 4 }}>
<Typography variant="h4" component="h1" gutterBottom align="center">
User Profile List
</Typography>
<UserProfileList />
</Box>
</Container>
</ThemeProvider>
);
}
export default App;
Implementing Advanced Avatar Features
Now that we have the basic implementation working, let's explore some advanced features of MUI Avatar to enhance our user profile list.
Creating Avatar Groups
MUI provides the AvatarGroup
component to display a stack of avatars. This is perfect for showing team members or participants in a conversation.
Let's create a component to display team members for each user:
import React from 'react';
import { AvatarGroup, Tooltip } from '@mui/material';
import UserAvatar from './UserAvatar';
// Let's assume each user might have team members
const mockTeamMembers = {
1: [users[1], users[2]], // John's team includes Jane and Robert
2: [users[0], users[4]], // Jane's team includes John and Michael
3: [users[1]], // Robert's team includes Jane
4: [users[0], users[2], users[4]], // Emily's team includes John, Robert, and Michael
5: [users[3]] // Michael's team includes Emily
};
const UserTeam = ({ userId, max = 3 }) => {
const teamMembers = mockTeamMembers[userId] || [];
if (teamMembers.length === 0) {
return null;
}
return (
<AvatarGroup max={max} sx={{ justifyContent: 'flex-end' }}>
{teamMembers.map(member => (
<Tooltip key={member.id} title={member.name}>
<div> {/* Wrapper div needed for Tooltip to work with Avatar */}
<UserAvatar
user={member}
size={24}
withStatus={false}
/>
</div>
</Tooltip>
))}
</AvatarGroup>
);
};
export default UserTeam;
Now, let's update our UserProfileList
to include the team members:
import React from 'react';
import {
List,
ListItem,
ListItemAvatar,
ListItemText,
ListItemSecondaryAction,
Typography,
Divider,
IconButton,
Chip,
Paper,
Box
} from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import UserAvatar from './UserAvatar';
import UserTeam from './UserTeam';
import { users } from './mockData';
const UserProfileList = () => {
return (
<Paper elevation={2} sx={{ maxWidth: 600, mx: 'auto', mt: 4 }}>
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
{users.map((user, index) => (
<React.Fragment key={user.id}>
<ListItem alignItems="flex-start">
<ListItemAvatar>
<UserAvatar user={user} />
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="subtitle1" component="span">
{user.name}
</Typography>
}
secondary={
<>
<Typography
component="span"
variant="body2"
color="text.primary"
sx={{ display: 'block' }}
>
{user.email}
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 0.5 }}>
<Chip
label={user.role}
size="small"
/>
<UserTeam userId={user.id} />
</Box>
</>
}
/>
<ListItemSecondaryAction>
<IconButton edge="end">
<MoreVertIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
{index < users.length - 1 && <Divider variant="inset" component="li" />}
</React.Fragment>
))}
</List>
</Paper>
);
};
export default UserProfileList;
Adding Avatar with Badges
We already implemented status badges, but let's enhance them with additional notification badges to indicate unread messages or alerts:
import React from 'react';
import { Avatar, Badge, styled } from '@mui/material';
// Get initials from a name
const getInitials = (name) => {
if (!name) return '?';
return name
.split(' ')
.map(word => word[0])
.join('')
.toUpperCase()
.substring(0, 2);
};
// Custom styled badge for status indicators
const StyledBadge = styled(Badge)(({ theme, status }) => ({
'& .MuiBadge-badge': {
backgroundColor:
status === 'active' ? '#44b700' :
status === 'away' ? '#ff9800' :
status === 'busy' ? '#f44336' :
'#bdbdbd',
color:
status === 'active' ? '#44b700' :
status === 'away' ? '#ff9800' :
status === 'busy' ? '#f44336' :
'#bdbdbd',
boxShadow: `0 0 0 2px ${theme.palette.background.paper}`,
'&::after': {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
borderRadius: '50%',
animation: status === 'active' ? 'ripple 1.2s infinite ease-in-out' : 'none',
border: '1px solid currentColor',
content: '""',
},
},
'@keyframes ripple': {
'0%': {
transform: 'scale(.8)',
opacity: 1,
},
'100%': {
transform: 'scale(2.4)',
opacity: 0,
},
},
}));
const NotificationBadge = styled(Badge)(({ theme }) => ({
'& .MuiBadge-badge': {
right: -3,
top: 13,
border: `2px solid ${theme.palette.background.paper}`,
padding: '0 4px',
},
}));
const UserAvatar = ({
user,
size = 40,
withStatus = true,
notifications = 0
}) => {
const { name, avatar, status } = user;
// Generate a consistent color based on the name
function stringToColor(string) {
if (!string) return '#757575';
let hash = 0;
for (let i = 0; i < string.length; i++) {
hash = string.charCodeAt(i) + ((hash << 5) - hash);
}
let color = '#';
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
color += `00${value.toString(16)}`.slice(-2);
}
return color;
}
const avatarContent = (
<Avatar
alt={name}
src={avatar}
sx={{
width: size,
height: size,
bgcolor: avatar ? 'transparent' : stringToColor(name),
fontSize: size * 0.4
}}
>
{getInitials(name)}
</Avatar>
);
// Apply notification badge if needed
let result = avatarContent;
if (notifications > 0) {
result = (
<NotificationBadge badgeContent={notifications} color="error">
{result}
</NotificationBadge>
);
}
// Apply status badge if needed
if (withStatus && status) {
result = (
<StyledBadge
overlap="circular"
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
variant="dot"
status={status}
>
{result}
</StyledBadge>
);
}
return result;
};
export default UserAvatar;
Now, let's update our mock data to include notifications:
export const users = [
{
id: 1,
name: "John Doe",
email: "john.doe@example.com",
avatar: "https://randomuser.me/api/portraits/men/1.jpg",
status: "active",
role: "Developer",
notifications: 3
},
{
id: 2,
name: "Jane Smith",
email: "jane.smith@example.com",
avatar: "https://randomuser.me/api/portraits/women/2.jpg",
status: "away",
role: "Designer",
notifications: 0
},
{
id: 3,
name: "Robert Johnson",
email: "robert.johnson@example.com",
avatar: "https://randomuser.me/api/portraits/men/3.jpg",
status: "offline",
role: "Manager",
notifications: 5
},
{
id: 4,
name: "Emily Davis",
email: "emily.davis@example.com",
avatar: "https://broken-link.jpg",
status: "active",
role: "Product Owner",
notifications: 0
},
{
id: 5,
name: "Michael Wilson",
email: "michael.wilson@example.com",
avatar: "https://randomuser.me/api/portraits/men/5.jpg",
status: "busy",
role: "QA Engineer",
notifications: 2
}
];
And finally, update our UserProfileList
component to pass the notifications prop:
<ListItemAvatar>
<UserAvatar
user={user}
notifications={user.notifications}
/>
</ListItemAvatar>
Implementing Avatar with Custom Styling
Let's create a variation of our avatar with custom styling to highlight VIP users:
import React from 'react';
import { Avatar, Badge, styled } from '@mui/material';
// VIP styled avatar with gold border
const VipAvatar = styled(Avatar)(({ theme }) => ({
border: '2px solid gold',
boxShadow: '0 0 8px rgba(255, 215, 0, 0.5)',
}));
const UserAvatar = ({
user,
size = 40,
withStatus = true,
notifications = 0,
isVip = false
}) => {
const { name, avatar, status } = user;
// Generate a consistent color based on the name
function stringToColor(string) {
if (!string) return '#757575';
let hash = 0;
for (let i = 0; i < string.length; i++) {
hash = string.charCodeAt(i) + ((hash << 5) - hash);
}
let color = '#';
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
color += `00${value.toString(16)}`.slice(-2);
}
return color;
}
// Choose the appropriate Avatar component
const AvatarComponent = isVip ? VipAvatar : Avatar;
const avatarContent = (
<AvatarComponent
alt={name}
src={avatar}
sx={{
width: size,
height: size,
bgcolor: avatar ? 'transparent' : stringToColor(name),
fontSize: size * 0.4
}}
>
{getInitials(name)}
</AvatarComponent>
);
// Rest of the component remains the same...
}
Building a Responsive User Profile Grid
So far, we've built a list view for our user profiles. Let's create an alternative grid layout that's more suitable for dashboards or member directories.
import React from 'react';
import {
Grid,
Card,
CardHeader,
CardContent,
Typography,
Box,
Chip,
IconButton,
useMediaQuery,
useTheme
} from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import EmailIcon from '@mui/icons-material/Email';
import UserAvatar from './UserAvatar';
import UserTeam from './UserTeam';
import { users } from './mockData';
const UserProfileGrid = () => {
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
return (
<Grid container spacing={3} sx={{ mt: 2 }}>
{users.map(user => (
<Grid item xs={12} sm={6} md={4} key={user.id}>
<Card
elevation={2}
sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
transition: 'transform 0.2s, box-shadow 0.2s',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: 6
}
}}
>
<CardHeader
avatar={
<UserAvatar
user={user}
size={isSmallScreen ? 40 : 56}
notifications={user.notifications}
isVip={user.id === 1} // Make John Doe a VIP
/>
}
action={
<IconButton aria-label="settings">
<MoreVertIcon />
</IconButton>
}
title={
<Typography variant="h6" component="div">
{user.name}
</Typography>
}
subheader={user.role}
/>
<CardContent sx={{ flexGrow: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<EmailIcon fontSize="small" sx={{ mr: 1, color: 'text.secondary' }} />
<Typography variant="body2" color="text.secondary">
{user.email}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 2 }}>
<Chip
label={user.status}
size="small"
color={
user.status === 'active' ? 'success' :
user.status === 'away' ? 'warning' :
user.status === 'busy' ? 'error' :
'default'
}
/>
<UserTeam userId={user.id} />
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
);
};
export default UserProfileGrid;
Now let's modify our App.js to allow switching between list and grid views:
import React, { useState } from 'react';
import {
ThemeProvider,
createTheme,
CssBaseline,
Container,
Typography,
Box,
ToggleButtonGroup,
ToggleButton
} from '@mui/material';
import ViewListIcon from '@mui/icons-material/ViewList';
import ViewModuleIcon from '@mui/icons-material/ViewModule';
import UserProfileList from './components/UserProfileList/UserProfileList';
import UserProfileGrid from './components/UserProfileList/UserProfileGrid';
// Create a theme instance
const theme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#1976d2',
},
secondary: {
main: '#dc004e',
},
},
});
function App() {
const [view, setView] = useState('list');
const handleChangeView = (event, nextView) => {
if (nextView !== null) {
setView(nextView);
}
};
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Container>
<Box sx={{ my: 4 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h4" component="h1">
User Directory
</Typography>
<ToggleButtonGroup
value={view}
exclusive
onChange={handleChangeView}
aria-label="view mode"
>
<ToggleButton value="list" aria-label="list view">
<ViewListIcon />
</ToggleButton>
<ToggleButton value="grid" aria-label="grid view">
<ViewModuleIcon />
</ToggleButton>
</ToggleButtonGroup>
</Box>
{view === 'list' ? <UserProfileList /> : <UserProfileGrid />}
</Box>
</Container>
</ThemeProvider>
);
}
export default App;
Handling Data Fetching and Loading States
In a real application, you'd fetch user data from an API. Let's implement that with proper loading states:
import React, { useState, useEffect } from 'react';
import {
List,
ListItem,
ListItemAvatar,
Skeleton,
Paper,
Typography,
Box,
Alert
} from '@mui/material';
import { users as mockUsers } from './mockData';
// Simulated API call with delay and potential errors
const fetchUsers = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
// Simulate successful API response 90% of the time
if (Math.random() > 0.1) {
resolve(mockUsers);
} else {
reject(new Error("Failed to fetch users"));
}
}, 1500); // 1.5 second delay to simulate network request
});
};
const UserProfileListWithFetch = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
fetchUsers()
.then(data => {
setUsers(data);
setLoading(false);
})
.catch(err => {
console.error("Error fetching users:", err);
setError(err.message);
setLoading(false);
});
}, []);
// Render loading skeletons
if (loading) {
return (
<Paper elevation={2} sx={{ maxWidth: 600, mx: 'auto', mt: 4 }}>
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
{[1, 2, 3, 4, 5].map((item) => (
<ListItem key={item} alignItems="flex-start">
<ListItemAvatar>
<Skeleton variant="circular" width={40} height={40} />
</ListItemAvatar>
<Box sx={{ width: '100%' }}>
<Skeleton variant="text" width="40%" height={24} />
<Skeleton variant="text" width="70%" height={20} />
<Skeleton variant="rectangular" width="30%" height={24} sx={{ mt: 1 }} />
</Box>
</ListItem>
))}
</List>
</Paper>
);
}
// Render error state
if (error) {
return (
<Alert severity="error" sx={{ maxWidth: 600, mx: 'auto', mt: 4 }}>
Error loading users: {error}
</Alert>
);
}
// Render actual list (same as before)
// ...
};
export default UserProfileListWithFetch;
Optimizing Performance
Let's implement some performance optimizations for our user profile list:
Virtualized List for Large Datasets
For lists with many users, we can use virtualization to render only visible items:
import React from 'react';
import {
ListItem,
ListItemAvatar,
ListItemText,
ListItemSecondaryAction,
Typography,
Divider,
IconButton,
Chip,
Paper,
Box
} from '@mui/material';
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import UserAvatar from './UserAvatar';
import { users } from './mockData';
// Let's create a larger dataset for demonstration
const largeUserList = Array(1000).fill().map((_, index) => {
// Cycle through our 5 mock users to create 1000 users
const templateUser = users[index % 5];
return {
...templateUser,
id: index + 1,
name: `${templateUser.name} ${Math.floor(index / 5) + 1}`,
email: `user${index + 1}@example.com`
};
});
// Row renderer for virtualized list
const UserRow = ({ index, style }) => {
const user = largeUserList[index];
return (
<div style={style}>
<ListItem alignItems="flex-start">
<ListItemAvatar>
<UserAvatar user={user} />
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="subtitle1" component="span">
{user.name}
</Typography>
}
secondary={
<>
<Typography
component="span"
variant="body2"
color="text.primary"
sx={{ display: 'block' }}
>
{user.email}
</Typography>
<Box sx={{ mt: 0.5 }}>
<Chip
label={user.role}
size="small"
/>
</Box>
</>
}
/>
<ListItemSecondaryAction>
<IconButton edge="end">
<MoreVertIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
<Divider variant="inset" component="li" />
</div>
);
};
const VirtualizedUserList = () => {
return (
<Paper elevation={2} sx={{ maxWidth: 600, mx: 'auto', mt: 4, height: 400 }}>
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
height={height}
width={width}
itemSize={90} // Adjust based on your row height
itemCount={largeUserList.length}
overscanCount={5}
>
{UserRow}
</FixedSizeList>
)}
</AutoSizer>
</Paper>
);
};
export default VirtualizedUserList;
Memoizing Components
For better performance, we should also memoize our components:
import React, { memo } from 'react';
import { Avatar, Badge, styled } from '@mui/material';
// Get initials from a name
const getInitials = (name) => {
if (!name) return '?';
return name
.split(' ')
.map(word => word[0])
.join('')
.toUpperCase()
.substring(0, 2);
};
// Custom styled badge for status indicators
const StyledBadge = styled(Badge)(({ theme, status }) => ({
// Same styling as before
}));
const UserAvatar = memo(({
user,
size = 40,
withStatus = true,
notifications = 0
}) => {
// Same implementation as before
});
export default UserAvatar;
Accessibility Enhancements
Let's improve the accessibility of our components:
import React, { memo } from 'react';
import { Avatar, Badge, styled, Tooltip } from '@mui/material';
// Get initials from a name
const getInitials = (name) => {
if (!name) return '?';
return name
.split(' ')
.map(word => word[0])
.join('')
.toUpperCase()
.substring(0, 2);
};
// Custom styled badge for status indicators
const StyledBadge = styled(Badge)(({ theme, status }) => ({
// Same styling as before
}));
const UserAvatar = memo(({
user,
size = 40,
withStatus = true,
notifications = 0,
ariaLabel
}) => {
const { name, avatar, status } = user;
// Generate color from name
function stringToColor(string) {
// Same implementation as before
}
// Prepare accessibility attributes
const a11yProps = {
'aria-label': ariaLabel || `Avatar for ${name}`,
role: 'img',
};
// If status is available, add it to the aria-label
if (withStatus && status) {
a11yProps['aria-label'] += `, status: ${status}`;
}
// If notifications are available, add them to the aria-label
if (notifications > 0) {
a11yProps['aria-label'] += `, ${notifications} notifications`;
}
const avatarContent = (
<Avatar
alt={name}
src={avatar}
sx={{
width: size,
height: size,
bgcolor: avatar ? 'transparent' : stringToColor(name),
fontSize: size * 0.4
}}
{...a11yProps}
>
{getInitials(name)}
</Avatar>
);
// Apply notification badge if needed
let result = avatarContent;
if (notifications > 0) {
result = (
<Badge
badgeContent={notifications}
color="error"
aria-label={`${notifications} unread notifications`}
>
{result}
</Badge>
);
}
// Apply status badge if needed
if (withStatus && status) {
result = (
<StyledBadge
overlap="circular"
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
variant="dot"
status={status}
>
{result}
</StyledBadge>
);
}
// Wrap in tooltip for additional context
return (
<Tooltip title={`${name} - ${status || 'unknown'} status`}>
<div>
{result}
</div>
</Tooltip>
);
});
export default UserAvatar;
Common Issues and Troubleshooting
When working with MUI Avatars, you might encounter some common issues. Here's how to address them:
Issue 1: Avatar Images Not Loading Correctly
Problem: Images don't load or display incorrectly.
Solution:
- Verify image URLs are correct and accessible
- Add proper error handling with fallbacks
- Ensure proper image dimensions to avoid distortion
// Enhanced error handling for image loading
const UserAvatar = ({ user, size = 40 }) => {
const [imgError, setImgError] = useState(false);
const { name, avatar } = user;
return (
<Avatar
alt={name}
src={imgError ? null : avatar}
onError={() => setImgError(true)}
sx={{ width: size, height: size }}
>
{getInitials(name)}
</Avatar>
);
};
Issue 2: Avatar Group Spacing Problems
Problem: Avatars in AvatarGroup have inconsistent spacing or overlap incorrectly.
Solution:
- Use the
spacing
prop to control the spacing between avatars - Ensure consistent avatar sizes within a group
- Use the
max
prop appropriately
<AvatarGroup
max={4}
spacing="small" // Can be 'small', 'medium', or a pixel value
sx={{
'& .MuiAvatar-root': {
width: 32,
height: 32,
fontSize: '1rem'
}
}}
>
{/* Avatars */}
</AvatarGroup>
Issue 3: Inconsistent Styling Across Different Browsers
Problem: Avatar appearance varies across browsers.
Solution:
- Use explicit dimensions and styles
- Apply consistent border-radius values
- Test across different browsers
<Avatar
sx={{
width: 40,
height: 40,
borderRadius: '50%', // Explicit border-radius
border: '1px solid rgba(0, 0, 0, 0.12)', // Consistent border
'& img': {
objectFit: 'cover', // Consistent image handling
}
}}
alt={name}
src={avatar}
>
{getInitials(name)}
</Avatar>
Best Practices for MUI Avatar Implementation
1. Always Provide Fallbacks
Always include fallback content (initials or icons) for when images fail to load:
<Avatar
alt="User Name"
src="/path/to/image.jpg"
>
UN
</Avatar>
2. Optimize Image Loading
For performance, consider lazy loading avatars and using proper image dimensions:
<Avatar
alt="User Name"
src="/path/to/image.jpg"
imgProps={{
loading: 'lazy',
srcSet: '/path/to/image-small.jpg 1x, /path/to/image.jpg 2x'
}}
>
UN
</Avatar>
3. Maintain Accessibility
Always include proper alt
text and ARIA attributes:
<Avatar
alt="Profile picture of Jane Smith"
src="/path/to/image.jpg"
aria-label="Jane Smith's profile"
>
JS
</Avatar>
4. Consistent Sizing
Maintain consistent sizing through your application by using theme variables:
// In your theme
const theme = createTheme({
components: {
MuiAvatar: {
styleOverrides: {
root: {
width: 40,
height: 40,
},
small: {
width: 24,
height: 24,
},
large: {
width: 56,
height: 56,
},
},
},
},
});
// In your component
<Avatar size="small">JS</Avatar>
<Avatar>JS</Avatar>
<Avatar size="large">JS</Avatar>
5. Performance Optimization
For lists with many avatars, use virtualization and memoization:
const MemoizedAvatar = React.memo(UserAvatar);
// Then in your list
{users.map(user => (
<MemoizedAvatar key={user.id} user={user} />
))}
Wrapping Up
In this comprehensive guide, we've explored how to use MUI's Avatar component to build a versatile user profile list with dynamic images. We've covered everything from basic implementation to advanced features like status badges, notification indicators, and responsive layouts. We've also addressed common issues and provided best practices for optimal performance and accessibility.
By implementing these techniques, you can create professional, responsive user interfaces that handle dynamic content gracefully. The Avatar component may seem simple at first glance, but as we've seen, it offers powerful capabilities when combined with other MUI components and proper React patterns.
Remember to always provide fallbacks for images, maintain accessibility, and optimize performance for the best user experience.