Building a Contact List with React MUI List: Complete Implementation Guide
As a front-end developer, you'll often need to display collections of data in a clean, interactive format. Contact lists are a common UI pattern, and Material UI's List component provides an excellent foundation for building them. In this article, I'll walk you through creating a fully functional contact list with selection capabilities and avatars using MUI's List components.
Learning Objectives
By the end of this guide, you'll be able to:
- Implement a contact list with MUI's List component family
- Add selection functionality (single and multiple)
- Incorporate avatars and secondary text
- Handle user interactions including clicks and selections
- Customize the appearance using MUI's styling system
- Implement virtualization for performance with large lists
- Add advanced features like search filtering and sorting
Understanding MUI's List Component System
Before diving into the implementation, let's understand the core components we'll be working with. MUI's List system consists of several specialized components that work together to create versatile list interfaces.
The List component in MUI is a container that renders a <ul>
element by default and is designed to display a collection of related items. It works in conjunction with ListItem, ListItemText, ListItemAvatar, and other related components to create rich list interfaces.
Let's explore the key components we'll use:
Core List Components
- List: The container component that wraps all list items.
- ListItem: The individual item in a list.
- ListItemButton: A wrapper that adds button functionality to list items.
- ListItemText: Displays primary and secondary text within a list item.
- ListItemAvatar: Displays an avatar within a list item.
- Checkbox/Radio: For selection functionality.
- Avatar: Displays user images or initials.
Each of these components serves a specific purpose in building a comprehensive contact list interface. Let's examine their props and behaviors in detail.
List Component Deep Dive
The List component is the foundation of our contact list UI. It provides the container for all list items and handles layout and spacing.
List Component Props
Prop | Type | Default | Description |
---|---|---|---|
children | node | - | The content of the component, normally ListItem elements. |
component | elementType | 'ul' | The component used for the root node. |
dense | bool | false | Decreases the padding to create a more compact list. |
disablePadding | bool | false | Removes padding from the list items. |
subheader | node | - | The content of the subheader, normally ListSubheader. |
sx | object | - | The system prop that allows defining system overrides. |
ListItem Component Props
Prop | Type | Default | Description |
---|---|---|---|
alignItems | 'flex-start' | 'center' | 'center' | Controls the alignment of items within the list item. |
button | bool | false | If true, the list item will be a button (deprecated - use ListItemButton). |
dense | bool | false | If true, compact vertical padding is used. |
disableGutters | bool | false | If true, the left and right padding is removed. |
divider | bool | false | If true, a divider is displayed below the list item. |
secondaryAction | node | - | The element to display at the end of the list item. |
selected | bool | false | If true, the list item will be selected. |
ListItemButton Props
Prop | Type | Default | Description |
---|---|---|---|
alignItems | 'flex-start' | 'center' | 'center' | Controls the alignment of items. |
autoFocus | bool | false | If true, the list item will be focused during the first mount. |
dense | bool | false | If true, compact vertical padding is used. |
disableGutters | bool | false | If true, the left and right padding is removed. |
divider | bool | false | If true, a divider is displayed below the list item. |
selected | bool | false | If true, the list item will be selected. |
ListItemText Props
Prop | Type | Default | Description |
---|---|---|---|
primary | node | - | The main text content. |
primaryTypographyProps | object | - | Props applied to the primary Typography element. |
secondary | node | - | The secondary text content. |
secondaryTypographyProps | object | - | Props applied to the secondary Typography element. |
inset | bool | false | If true, the children will be indented. |
ListItemAvatar Props
Prop | Type | Default | Description |
---|---|---|---|
children | node | - | The content of the component, normally an Avatar. |
Setting Up the Project
Let's start by setting up a new React project and installing the necessary dependencies.
Creating a New React Project
First, we'll create a new React application using Create React App:
npx create-react-app contact-list-mui
cd contact-list-mui
Installing Material UI Dependencies
Next, we'll install Material UI and its icon package:
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled
Now that we have our project set up, let's start building our contact list component.
Building a Basic Contact List
Let's start with a simple contact list that displays names and avatars.
Step 1: Create the Contact Data
First, we'll create some mock data for our contacts:
// src/data/contacts.js
const contacts = [
{
id: 1,
name: "John Smith",
email: "john.smith@example.com",
phone: "+1 (555) 123-4567",
avatar: "https://mui.com/static/images/avatar/1.jpg",
},
{
id: 2,
name: "Sarah Johnson",
email: "sarah.johnson@example.com",
phone: "+1 (555) 234-5678",
avatar: "https://mui.com/static/images/avatar/2.jpg",
},
{
id: 3,
name: "Michael Brown",
email: "michael.brown@example.com",
phone: "+1 (555) 345-6789",
avatar: "https://mui.com/static/images/avatar/3.jpg",
},
{
id: 4,
name: "Emily Davis",
email: "emily.davis@example.com",
phone: "+1 (555) 456-7890",
avatar: "https://mui.com/static/images/avatar/4.jpg",
},
{
id: 5,
name: "Daniel Wilson",
email: "daniel.wilson@example.com",
phone: "+1 (555) 567-8901",
avatar: "https://mui.com/static/images/avatar/5.jpg",
},
];
export default contacts;
This data structure includes all the information we'll need for our contact list: unique IDs, names, contact information, and avatar URLs.
Step 2: Create a Basic Contact List Component
Now, let's create a basic contact list component that displays our contacts:
// src/components/BasicContactList.jsx
import React from 'react';
import {
List,
ListItem,
ListItemText,
ListItemAvatar,
Avatar,
Paper,
Typography
} from '@mui/material';
import contacts from '../data/contacts';
function BasicContactList() {
return (
<Paper elevation={3} sx={{ maxWidth: 400, mx: 'auto', mt: 4 }}>
<Typography variant="h6" sx={{ p: 2, bgcolor: 'primary.main', color: 'primary.contrastText' }}>
Contacts
</Typography>
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
{contacts.map((contact) => (
<ListItem key={contact.id} divider>
<ListItemAvatar>
<Avatar alt={contact.name} src={contact.avatar} />
</ListItemAvatar>
<ListItemText
primary={contact.name}
secondary={contact.email}
/>
</ListItem>
))}
</List>
</Paper>
);
}
export default BasicContactList;
Let's update our App.js
to display this component:
// src/App.js
import React from 'react';
import BasicContactList from './components/BasicContactList';
import { CssBaseline, Container } from '@mui/material';
function App() {
return (
<>
<CssBaseline />
<Container>
<BasicContactList />
</Container>
</>
);
}
export default App;
This gives us a simple contact list with avatars and basic contact information. Let's break down what's happening:
- We're using the
List
component as a container for our list items. - Each contact is represented by a
ListItem
with adivider
to separate entries. ListItemAvatar
andAvatar
components display the contact's image.ListItemText
displays the contact's name as primary text and email as secondary text.- We've wrapped everything in a
Paper
component with some styling for a clean look.
Adding Selection Functionality
Now, let's enhance our contact list by adding selection functionality. We'll implement both single and multiple selection modes.
Creating a Selectable Contact List
Let's create a new component for a selectable contact list:
// src/components/SelectableContactList.jsx
import React, { useState } from 'react';
import {
List,
ListItemButton,
ListItemText,
ListItemAvatar,
Avatar,
Paper,
Typography,
Checkbox,
IconButton,
Divider,
Box,
Switch,
FormControlLabel
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import contacts from '../data/contacts';
function SelectableContactList() {
const [selected, setSelected] = useState([]);
const [multiSelect, setMultiSelect] = useState(false);
const handleToggle = (id) => () => {
if (multiSelect) {
// For multiple selection
const currentIndex = selected.indexOf(id);
const newSelected = [...selected];
if (currentIndex === -1) {
newSelected.push(id);
} else {
newSelected.splice(currentIndex, 1);
}
setSelected(newSelected);
} else {
// For single selection
setSelected(selected.includes(id) ? [] : [id]);
}
};
const handleDeleteSelected = () => {
console.log(`Deleting contacts: ${selected.join(', ')}`);
// In a real app, you would call an API to delete these contacts
setSelected([]);
};
const handleToggleMultiSelect = () => {
setMultiSelect(!multiSelect);
setSelected([]); // Clear selections when switching modes
};
return (
<Paper elevation={3} sx={{ maxWidth: 400, mx: 'auto', mt: 4 }}>
<Box sx={{ p: 2, bgcolor: 'primary.main', color: 'primary.contrastText', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="h6">
Contacts ({selected.length} selected)
</Typography>
{selected.length > 0 && (
<IconButton
color="inherit"
onClick={handleDeleteSelected}
aria-label="delete selected contacts"
>
<DeleteIcon />
</IconButton>
)}
</Box>
<Box sx={{ p: 1, bgcolor: 'background.paper' }}>
<FormControlLabel
control={
<Switch
checked={multiSelect}
onChange={handleToggleMultiSelect}
color="primary"
/>
}
label="Multiple selection"
/>
</Box>
<Divider />
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
{contacts.map((contact) => {
const isSelected = selected.indexOf(contact.id) !== -1;
return (
<ListItemButton
key={contact.id}
divider
selected={isSelected}
onClick={handleToggle(contact.id)}
dense
>
<Checkbox
edge="start"
checked={isSelected}
tabIndex={-1}
disableRipple
inputProps={{ 'aria-labelledby': `checkbox-list-label-${contact.id}` }}
/>
<ListItemAvatar>
<Avatar alt={contact.name} src={contact.avatar} />
</ListItemAvatar>
<ListItemText
id={`checkbox-list-label-${contact.id}`}
primary={contact.name}
secondary={contact.email}
/>
</ListItemButton>
);
})}
</List>
</Paper>
);
}
export default SelectableContactList;
Now, let's update our App.js
to use this new component:
// src/App.js
import React from 'react';
import SelectableContactList from './components/SelectableContactList';
import { CssBaseline, Container } from '@mui/material';
function App() {
return (
<>
<CssBaseline />
<Container>
<SelectableContactList />
</Container>
</>
);
}
export default App;
Let's analyze the key aspects of our selectable contact list:
- State Management: We use React's
useState
hook to track selected contacts and the selection mode. - Selection Handling: The
handleToggle
function manages both single and multiple selection modes. - ListItemButton: We've replaced
ListItem
withListItemButton
to make the entire list item clickable. - Checkbox Component: We've added checkboxes to indicate selection status.
- Visual Feedback: The
selected
prop onListItemButton
provides visual feedback for selected items. - Action Buttons: We've added a delete button that appears when items are selected.
- Selection Mode Toggle: A switch allows users to toggle between single and multiple selection modes.
This implementation gives users the flexibility to select contacts in different ways based on their needs.
Advanced Contact List with Detailed Information
Let's create a more advanced contact list that displays additional information and provides more interaction options.
// src/components/AdvancedContactList.jsx
import React, { useState } from 'react';
import {
List,
ListItemButton,
ListItemText,
ListItemAvatar,
ListItemSecondaryAction,
Avatar,
Paper,
Typography,
IconButton,
Divider,
Box,
Collapse,
TextField,
InputAdornment,
Chip
} from '@mui/material';
import {
Phone as PhoneIcon,
Email as EmailIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
Search as SearchIcon,
Star as StarIcon,
StarBorder as StarBorderIcon
} from '@mui/icons-material';
import contacts from '../data/contacts';
function AdvancedContactList() {
const [expanded, setExpanded] = useState({});
const [searchQuery, setSearchQuery] = useState('');
const [favorites, setFavorites] = useState([]);
const handleToggleExpand = (id) => {
setExpanded(prev => ({
...prev,
[id]: !prev[id]
}));
};
const handleToggleFavorite = (id, event) => {
event.stopPropagation(); // Prevent triggering the expand/collapse
setFavorites(prev => {
if (prev.includes(id)) {
return prev.filter(favId => favId !== id);
} else {
return [...prev, id];
}
});
};
const handleSearch = (event) => {
setSearchQuery(event.target.value);
};
// Filter contacts based on search query
const filteredContacts = contacts.filter(contact => {
const searchLower = searchQuery.toLowerCase();
return (
contact.name.toLowerCase().includes(searchLower) ||
contact.email.toLowerCase().includes(searchLower) ||
contact.phone.includes(searchQuery)
);
});
// Sort contacts: favorites first, then alphabetically
const sortedContacts = [...filteredContacts].sort((a, b) => {
// First sort by favorite status
const aFav = favorites.includes(a.id) ? 1 : 0;
const bFav = favorites.includes(b.id) ? 1 : 0;
if (bFav !== aFav) {
return bFav - aFav; // Favorites first
}
// Then sort alphabetically
return a.name.localeCompare(b.name);
});
return (
<Paper elevation={3} sx={{ maxWidth: 500, mx: 'auto', mt: 4 }}>
<Box sx={{ p: 2, bgcolor: 'primary.main', color: 'primary.contrastText' }}>
<Typography variant="h6">
Contact Directory
</Typography>
</Box>
<Box sx={{ p: 2, bgcolor: 'background.paper' }}>
<TextField
fullWidth
placeholder="Search contacts..."
variant="outlined"
size="small"
value={searchQuery}
onChange={handleSearch}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Box>
<Divider />
{favorites.length > 0 && (
<Box sx={{ p: 1, bgcolor: 'background.paper' }}>
<Typography variant="subtitle2" color="text.secondary">
Favorites: {favorites.length}
</Typography>
</Box>
)}
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
{sortedContacts.length === 0 ? (
<Box sx={{ p: 2, textAlign: 'center' }}>
<Typography color="text.secondary">No contacts found</Typography>
</Box>
) : (
sortedContacts.map((contact) => {
const isExpanded = expanded[contact.id] || false;
const isFavorite = favorites.includes(contact.id);
return (
<React.Fragment key={contact.id}>
<ListItemButton
onClick={() => handleToggleExpand(contact.id)}
sx={{
bgcolor: isFavorite ? 'rgba(255, 215, 0, 0.1)' : 'inherit',
}}
>
<ListItemAvatar>
<Avatar alt={contact.name} src={contact.avatar} />
</ListItemAvatar>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{contact.name}
{isFavorite && (
<Chip
size="small"
label="Favorite"
color="primary"
sx={{ ml: 1, height: 20 }}
/>
)}
</Box>
}
secondary={contact.email}
/>
<ListItemSecondaryAction>
<IconButton
edge="end"
onClick={(e) => handleToggleFavorite(contact.id, e)}
color={isFavorite ? "primary" : "default"}
>
{isFavorite ? <StarIcon /> : <StarBorderIcon />}
</IconButton>
{isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</ListItemSecondaryAction>
</ListItemButton>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Box sx={{ p: 2, pl: 9, bgcolor: 'action.hover' }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<EmailIcon fontSize="small" sx={{ mr: 1, color: 'text.secondary' }} />
<Typography variant="body2">{contact.email}</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<PhoneIcon fontSize="small" sx={{ mr: 1, color: 'text.secondary' }} />
<Typography variant="body2">{contact.phone}</Typography>
</Box>
</Box>
</Collapse>
<Divider />
</React.Fragment>
);
})
)}
</List>
</Paper>
);
}
export default AdvancedContactList;
Update our App.js
to use this advanced component:
// src/App.js
import React from 'react';
import AdvancedContactList from './components/AdvancedContactList';
import { CssBaseline, Container } from '@mui/material';
function App() {
return (
<>
<CssBaseline />
<Container>
<AdvancedContactList />
</Container>
</>
);
}
export default App;
This advanced contact list includes several sophisticated features:
- Expandable Details: Users can click on contacts to expand and see more details.
- Search Functionality: Users can search for contacts by name, email, or phone number.
- Favorites: Users can mark contacts as favorites, which are displayed at the top of the list.
- Visual Indicators: Icons and color changes indicate expanded and favorite status.
- Empty State Handling: A message is displayed when no contacts match the search criteria.
The implementation demonstrates how to combine various MUI components to create a rich, interactive interface.
Building a Virtualized Contact List for Performance
When dealing with large lists, performance can become an issue. MUI doesn't include virtualization out of the box, but we can use react-window
to implement it. Let's create a virtualized contact list:
First, install the required package:
npm install react-window
Now, let's create the virtualized component:
// src/components/VirtualizedContactList.jsx
import React, { useState } from 'react';
import { FixedSizeList } from 'react-window';
import {
ListItemButton,
ListItemText,
ListItemAvatar,
Avatar,
Paper,
Typography,
Checkbox,
Box,
Divider
} from '@mui/material';
// Generate a large number of contacts for demonstration
const generateLargeContactList = (count) => {
return Array.from({ length: count }, (_, index) => ({
id: index + 1,
name: `Contact ${index + 1}`,
email: `contact${index + 1}@example.com`,
phone: `+1 (555) ${100 + index}-${1000 + index}`,
avatar: `https://mui.com/static/images/avatar/${(index % 8) + 1}.jpg`,
}));
};
const largeContactList = generateLargeContactList(1000);
function VirtualizedContactList() {
const [selected, setSelected] = useState([]);
const handleToggle = (id) => {
const currentIndex = selected.indexOf(id);
const newSelected = [...selected];
if (currentIndex === -1) {
newSelected.push(id);
} else {
newSelected.splice(currentIndex, 1);
}
setSelected(newSelected);
};
// Render an individual row
const renderRow = ({ index, style }) => {
const contact = largeContactList[index];
const isSelected = selected.indexOf(contact.id) !== -1;
return (
<ListItemButton
style={style}
selected={isSelected}
onClick={() => handleToggle(contact.id)}
divider
>
<Checkbox
edge="start"
checked={isSelected}
tabIndex={-1}
disableRipple
/>
<ListItemAvatar>
<Avatar alt={contact.name} src={contact.avatar} />
</ListItemAvatar>
<ListItemText
primary={contact.name}
secondary={contact.email}
/>
</ListItemButton>
);
};
return (
<Paper elevation={3} sx={{ maxWidth: 500, mx: 'auto', mt: 4 }}>
<Box sx={{ p: 2, bgcolor: 'primary.main', color: 'primary.contrastText' }}>
<Typography variant="h6">
Large Contact List ({largeContactList.length} contacts)
</Typography>
</Box>
<Divider />
<Box sx={{ height: 400, width: '100%' }}>
<FixedSizeList
height={400}
width="100%"
itemSize={72} // Height of each row
itemCount={largeContactList.length}
overscanCount={5} // Number of items to render outside of the visible area
>
{renderRow}
</FixedSizeList>
</Box>
</Paper>
);
}
export default VirtualizedContactList;
Update our App.js
to use this virtualized component:
// src/App.js
import React from 'react';
import VirtualizedContactList from './components/VirtualizedContactList';
import { CssBaseline, Container } from '@mui/material';
function App() {
return (
<>
<CssBaseline />
<Container>
<VirtualizedContactList />
</Container>
</>
);
}
export default App;
The virtualized list offers significant performance improvements when working with large datasets:
- Efficient Rendering: Only the visible items (and a few extra for smooth scrolling) are actually rendered in the DOM.
- Reduced Memory Usage: Since fewer DOM elements are created, memory usage is reduced.
- Smooth Scrolling: The virtualization library handles efficient updates during scrolling.
This approach is crucial for maintaining good performance when dealing with hundreds or thousands of contacts.
Creating a Complete Contact Management Application
Let's combine everything we've learned to create a comprehensive contact management application with tabs for different views:
// src/components/ContactManagementApp.jsx
import React, { useState } from 'react';
import {
Tabs,
Tab,
Box,
Paper,
Typography,
List,
ListItemButton,
ListItemText,
ListItemAvatar,
ListItemSecondaryAction,
Avatar,
IconButton,
TextField,
InputAdornment,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Divider,
Checkbox,
Snackbar,
Alert
} from '@mui/material';
import {
Add as AddIcon,
Delete as DeleteIcon,
Edit as EditIcon,
Search as SearchIcon,
Star as StarIcon,
StarBorder as StarBorderIcon,
Person as PersonIcon
} from '@mui/icons-material';
import contacts from '../data/contacts';
function ContactManagementApp() {
// State
const [value, setValue] = useState(0);
const [contactList, setContactList] = useState(contacts);
const [searchQuery, setSearchQuery] = useState('');
const [selected, setSelected] = useState([]);
const [favorites, setFavorites] = useState([]);
const [openDialog, setOpenDialog] = useState(false);
const [currentContact, setCurrentContact] = useState(null);
const [snackbar, setSnackbar] = useState({ open: false, message: '', severity: 'success' });
// Form state
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
avatar: ''
});
// Handlers
const handleTabChange = (event, newValue) => {
setValue(newValue);
};
const handleSearch = (event) => {
setSearchQuery(event.target.value);
};
const handleToggleSelect = (id) => {
const currentIndex = selected.indexOf(id);
const newSelected = [...selected];
if (currentIndex === -1) {
newSelected.push(id);
} else {
newSelected.splice(currentIndex, 1);
}
setSelected(newSelected);
};
const handleToggleFavorite = (id, event) => {
if (event) event.stopPropagation();
setFavorites(prev => {
if (prev.includes(id)) {
return prev.filter(favId => favId !== id);
} else {
return [...prev, id];
}
});
};
const handleDeleteSelected = () => {
setContactList(prev => prev.filter(contact => !selected.includes(contact.id)));
setSelected([]);
showSnackbar('Contacts deleted successfully', 'success');
};
const handleOpenDialog = (contact = null) => {
if (contact) {
setCurrentContact(contact);
setFormData({
name: contact.name,
email: contact.email,
phone: contact.phone,
avatar: contact.avatar
});
} else {
setCurrentContact(null);
setFormData({
name: '',
email: '',
phone: '',
avatar: ''
});
}
setOpenDialog(true);
};
const handleCloseDialog = () => {
setOpenDialog(false);
};
const handleFormChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSaveContact = () => {
if (currentContact) {
// Edit existing contact
setContactList(prev =>
prev.map(contact =>
contact.id === currentContact.id
? { ...contact, ...formData }
: contact
)
);
showSnackbar('Contact updated successfully', 'success');
} else {
// Add new contact
const newContact = {
id: Date.now(), // Simple way to generate unique ID
...formData
};
setContactList(prev => [...prev, newContact]);
showSnackbar('Contact added successfully', 'success');
}
setOpenDialog(false);
};
const showSnackbar = (message, severity) => {
setSnackbar({ open: true, message, severity });
};
const handleCloseSnackbar = () => {
setSnackbar({ ...snackbar, open: false });
};
// Filter contacts based on search and current tab
const filteredContacts = contactList.filter(contact => {
const matchesSearch = searchQuery === '' ||
contact.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
contact.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
contact.phone.includes(searchQuery);
if (value === 0) return matchesSearch; // All contacts
if (value === 1) return matchesSearch && favorites.includes(contact.id); // Favorites only
return false;
});
return (
<Paper elevation={3} sx={{ maxWidth: 600, mx: 'auto', mt: 4 }}>
{/* Header */}
<Box sx={{ p: 2, bgcolor: 'primary.main', color: 'primary.contrastText', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="h6">
Contact Manager
</Typography>
<Box>
{selected.length > 0 ? (
<IconButton
color="inherit"
onClick={handleDeleteSelected}
aria-label="delete selected contacts"
>
<DeleteIcon />
</IconButton>
) : (
<IconButton
color="inherit"
onClick={() => handleOpenDialog()}
aria-label="add new contact"
>
<AddIcon />
</IconButton>
)}
</Box>
</Box>
{/* Tabs */}
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleTabChange} aria-label="contact tabs">
<Tab label="All Contacts" id="tab-0" aria-controls="tabpanel-0" />
<Tab label="Favorites" id="tab-1" aria-controls="tabpanel-1" />
</Tabs>
</Box>
{/* Search */}
<Box sx={{ p: 2 }}>
<TextField
fullWidth
placeholder="Search contacts..."
variant="outlined"
size="small"
value={searchQuery}
onChange={handleSearch}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Box>
<Divider />
{/* Contact List */}
<Box role="tabpanel" id={`tabpanel-${value}`} aria-labelledby={`tab-${value}`}>
<List sx={{ width: '100%', bgcolor: 'background.paper', maxHeight: 400, overflow: 'auto' }}>
{filteredContacts.length === 0 ? (
<Box sx={{ p: 3, textAlign: 'center' }}>
<Typography color="text.secondary">
{value === 0
? 'No contacts found. Add a new contact to get started.'
: 'No favorite contacts found.'}
</Typography>
</Box>
) : (
filteredContacts.map((contact) => {
const isSelected = selected.includes(contact.id);
const isFavorite = favorites.includes(contact.id);
return (
<ListItemButton
key={contact.id}
selected={isSelected}
onClick={() => handleToggleSelect(contact.id)}
divider
>
<Checkbox
edge="start"
checked={isSelected}
tabIndex={-1}
disableRipple
onClick={(e) => e.stopPropagation()}
onChange={() => handleToggleSelect(contact.id)}
/>
<ListItemAvatar>
<Avatar
alt={contact.name}
src={contact.avatar}
sx={{ bgcolor: !contact.avatar ? 'primary.main' : undefined }}
>
{!contact.avatar && <PersonIcon />}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={contact.name}
secondary={
<>
<Typography component="span" variant="body2" color="text.primary">
{contact.email}
</Typography>
{" — "}{contact.phone}
</>
}
/>
<ListItemSecondaryAction>
<IconButton
edge="end"
onClick={(e) => handleToggleFavorite(contact.id, e)}
color={isFavorite ? "primary" : "default"}
>
{isFavorite ? <StarIcon /> : <StarBorderIcon />}
</IconButton>
<IconButton
edge="end"
onClick={(e) => {
e.stopPropagation();
handleOpenDialog(contact);
}}
>
<EditIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItemButton>
);
})
)}
</List>
</Box>
{/* Add/Edit Contact Dialog */}
<Dialog open={openDialog} onClose={handleCloseDialog} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">
{currentContact ? 'Edit Contact' : 'Add New Contact'}
</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
name="name"
label="Name"
type="text"
fullWidth
value={formData.name}
onChange={handleFormChange}
required
/>
<TextField
margin="dense"
name="email"
label="Email Address"
type="email"
fullWidth
value={formData.email}
onChange={handleFormChange}
required
/>
<TextField
margin="dense"
name="phone"
label="Phone Number"
type="tel"
fullWidth
value={formData.phone}
onChange={handleFormChange}
required
/>
<TextField
margin="dense"
name="avatar"
label="Avatar URL"
type="url"
fullWidth
value={formData.avatar}
onChange={handleFormChange}
helperText="Leave empty to use initials"
/>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog} color="primary">
Cancel
</Button>
<Button
onClick={handleSaveContact}
color="primary"
disabled={!formData.name || !formData.email || !formData.phone}
>
Save
</Button>
</DialogActions>
</Dialog>
{/* Snackbar for notifications */}
<Snackbar
open={snackbar.open}
autoHideDuration={6000}
onClose={handleCloseSnackbar}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert onClose={handleCloseSnackbar} severity={snackbar.severity} sx={{ width: '100%' }}>
{snackbar.message}
</Alert>
</Snackbar>
</Paper>
);
}
export default ContactManagementApp;
Update our App.js
to use this comprehensive component:
// src/App.js
import React from 'react';
import ContactManagementApp from './components/ContactManagementApp';
import { CssBaseline, Container } from '@mui/material';
function App() {
return (
<>
<CssBaseline />
<Container>
<ContactManagementApp />
</Container>
</>
);
}
export default App;
This comprehensive contact management application includes:
- Tab Navigation: Allows users to switch between all contacts and favorites.
- CRUD Operations: Users can create, read, update, and delete contacts.
- Search Functionality: Users can search for contacts.
- Selection and Batch Operations: Users can select multiple contacts and delete them at once.
- Favorites Management: Users can mark contacts as favorites.
- Form Validation: Basic validation for required fields.
- Feedback System: Snackbars provide feedback on actions.
The application demonstrates how to combine various MUI components into a cohesive, feature-rich interface.
Customizing MUI List Appearance with Theming
MUI provides powerful theming capabilities that allow you to customize the appearance of components across your application. Let's explore how to customize our contact list using MUI's theming system:
// src/theme/theme.js
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
palette: {
primary: {
main: '#1976d2',
light: '#4791db',
dark: '#115293',
contrastText: '#fff',
},
secondary: {
main: '#dc004e',
light: '#e33371',
dark: '#9a0036',
contrastText: '#fff',
},
background: {
default: '#f5f5f5',
paper: '#fff',
},
},
components: {
// Customize List component
MuiList: {
styleOverrides: {
root: {
padding: 0,
},
},
},
// Customize ListItem component
MuiListItem: {
styleOverrides: {
root: {
'&.Mui-selected': {
backgroundColor: 'rgba(25, 118, 210, 0.08)',
},
},
},
},
// Customize ListItemButton component
MuiListItemButton: {
styleOverrides: {
root: {
'&.Mui-selected': {
backgroundColor: 'rgba(25, 118, 210, 0.08)',
'&:hover': {
backgroundColor: 'rgba(25, 118, 210, 0.12)',
},
},
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.04)',
},
},
},
},
// Customize Avatar component
MuiAvatar: {
styleOverrides: {
root: {
width: 40,
height: 40,
},
},
},
// Customize Divider component
MuiDivider: {
styleOverrides: {
root: {
margin: 0,
},
},
},
},
typography: {
h6: {
fontWeight: 500,
},
subtitle1: {
fontWeight: 500,
},
body2: {
color: 'rgba(0, 0, 0, 0.6)',
},
},
});
export default theme;
Now, let's update our App.js
to use this theme:
// src/App.js
import React from 'react';
import { ThemeProvider } from '@mui/material/styles';
import { CssBaseline, Container } from '@mui/material';
import ContactManagementApp from './components/ContactManagementApp';
import theme from './theme/theme';
function App() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Container>
<ContactManagementApp />
</Container>
</ThemeProvider>
);
}
export default App;
This theming approach offers several benefits:
- Consistent Styling: The theme ensures consistent styling across all instances of the components.
- Global Customization: You can change the appearance of all instances of a component without modifying each one individually.
- Maintainable Code: Centralizing style definitions makes it easier to maintain and update your application's appearance.
- Brand Alignment: You can align the component styles with your brand guidelines.
MUI's theming system is particularly powerful because it allows for deep customization while maintaining the component's functionality and accessibility features.
Accessibility Considerations for MUI Lists
Accessibility is a crucial aspect of modern web applications. Let's enhance our contact list to ensure it's accessible to all users:
// src/components/AccessibleContactList.jsx
import React, { useState } from 'react';
import {
List,
ListItemButton,
ListItemText,
ListItemAvatar,
Avatar,
Paper,
Typography,
Box,
Divider,
VisuallyHidden
} from '@mui/material';
import contacts from '../data/contacts';
// Custom component for visually hidden content
const VisuallyHidden = ({ children }) => (
<Box sx={{
border: 0,
clip: 'rect(0 0 0 0)',
height: '1px',
margin: -1,
overflow: 'hidden',
padding: 0,
position: 'absolute',
width: '1px',
whiteSpace: 'nowrap'
}}>
{children}
</Box>
);
function AccessibleContactList() {
const [selectedId, setSelectedId] = useState(null);
const handleListItemClick = (id) => {
setSelectedId(id);
};
return (
<Paper elevation={3} sx={{ maxWidth: 400, mx: 'auto', mt: 4 }}>
<Box
component="header"
sx={{ p: 2, bgcolor: 'primary.main', color: 'primary.contrastText' }}
role="banner"
>
<Typography variant="h6" component="h1">
Contacts
</Typography>
</Box>
<Divider />
<Box role="region" aria-label="Contact list">
<List
sx={{ width: '100%', bgcolor: 'background.paper' }}
aria-label="contacts"
role="listbox"
>
{contacts.map((contact) => (
<ListItemButton
key={contact.id}
selected={selectedId === contact.id}
onClick={() => handleListItemClick(contact.id)}
role="option"
aria-selected={selectedId === contact.id}
divider
>
<ListItemAvatar>
<Avatar
alt={contact.name}
src={contact.avatar}
aria-hidden="true" // Avatar is decorative
/>
</ListItemAvatar>
<ListItemText
primary={contact.name}
secondary={
<>
<VisuallyHidden>Email: </VisuallyHidden>
{contact.email}
</>
}
primaryTypographyProps={{
'aria-label': `Contact name: ${contact.name}`
}}
secondaryTypographyProps={{
'aria-label': `Email: ${contact.email}`
}}
/>
</ListItemButton>
))}
</List>
</Box>
</Paper>
);
}
export default AccessibleContactList;
The accessibility enhancements include:
- ARIA Roles: Adding appropriate roles like
listbox
andoption
to clarify the component's purpose. - ARIA States: Using
aria-selected
to indicate the selected state. - Screen Reader Text: Adding context for screen readers with
VisuallyHidden
components. - Semantic Structure: Using appropriate landmark roles like
banner
andregion
. - Descriptive Labels: Providing clear labels for screen readers with
aria-label
.
These enhancements ensure that users with disabilities can effectively use our contact list, improving the overall user experience for everyone.
Best Practices and Common Issues
When working with MUI's List components, keep these best practices in mind:
Best Practices for MUI Lists
-
Use the Right Components for the Job:
- Use
ListItemButton
for clickable items - Use
ListItemText
for primary and secondary text - Use
ListItemAvatar
for avatars or icons
- Use
-
Maintain Proper Nesting:
- Always nest
ListItem
directly insideList
- Place
ListItemText
,ListItemAvatar
, etc. insideListItem
- Always nest
-
Handle Selection States Properly:
- Use the
selected
prop for visual feedback - Maintain selection state in your component's state
- Provide clear visual and accessibility cues for selection
- Use the
-
Optimize for Performance:
- Use virtualization for long lists
- Implement efficient filtering and sorting
- Avoid unnecessary re-renders by using memoization
-
Ensure Accessibility:
- Provide proper ARIA attributes
- Ensure keyboard navigation works
- Test with screen readers
Common Issues and Solutions
-
Issue: List items not clickable Solution: Use
ListItemButton
instead ofListItem
for clickable items -
Issue: Secondary actions triggering main click handler Solution: Use
event.stopPropagation()
in secondary action handlers -
Issue: Poor performance with large lists Solution: Implement virtualization with
react-window
or similar libraries -
Issue: Inconsistent styling Solution: Use MUI's theming system for consistent styling
-
Issue: Selection state not working correctly Solution: Ensure you're properly tracking selected items in state and applying the
selected
prop
Wrapping Up
In this comprehensive guide, we've explored how to build various types of contact lists using MUI's List components. We've covered everything from basic lists to advanced implementations with selection, virtualization, and comprehensive CRUD operations.
We started with the fundamentals of MUI's List component system and progressively built more complex interfaces. Along the way, we explored important concepts like state management, performance optimization, theming, and accessibility.
The MUI List component family provides a powerful foundation for building rich, interactive lists in your React applications. By combining these components with React's state management and MUI's styling system, you can create polished, performant user interfaces that meet your specific requirements.
Remember to consider performance for large lists, ensure accessibility for all users, and maintain a consistent design through theming. With these considerations in mind, you'll be able to build contact lists that are both functional and delightful to use.