Building Interactive Galleries with MUI ImageList: Hover Effects and Beyond
Creating an engaging image gallery is a common requirement in modern web applications. Whether you're building a portfolio, e-commerce product display, or a photo-sharing platform, Material UI's ImageList component provides a powerful foundation for displaying collections of images. In this article, I'll show you how to leverage MUI's ImageList component to create a responsive gallery with custom hover effects that will elevate your user interface.
What You'll Learn
By the end of this tutorial, you'll be able to:
- Implement MUI's ImageList component with various layouts
- Create custom hover effects that reveal image information
- Build responsive galleries that adapt to different screen sizes
- Apply advanced customization techniques with the sx prop and styled components
- Implement performance optimizations for image-heavy applications
- Handle common edge cases and accessibility concerns
Understanding MUI's ImageList Component
The ImageList component (formerly known as GridList in MUI v4) is a versatile tool for displaying a collection of images in an organized grid layout. It's particularly useful when you need to present multiple images with consistent spacing and alignment.
Core Components and Structure
MUI's ImageList system consists of three main components that work together:
- ImageList: The container component that manages the layout and spacing of its children.
- ImageListItem: The individual items that wrap each image.
- ImageListItemBar: An optional overlay that can display information about the image.
This modular structure gives you flexibility to customize each part independently while maintaining a cohesive design.
Available Variants and Layouts
MUI's ImageList supports three built-in layout variants that determine how images are arranged:
- Standard (default): A simple grid where all items have the same size.
- Quilted: A Pinterest-style layout where items can span multiple rows or columns.
- Masonry: A layout that maintains the original aspect ratio of images while aligning them into columns.
- Woven: A layout that alternates the position of images in a woven pattern.
Each layout offers different visual aesthetics and is suitable for different types of content. The standard layout works well for uniform content, while quilted and masonry layouts are better for diverse image collections with varying dimensions.
Essential Props and Configurations
Let's examine the key props that control the ImageList's behavior:
Prop | Type | Default | Description |
---|---|---|---|
variant | 'standard' | 'quilted' | 'masonry' | 'woven' | 'standard' | Determines the layout arrangement of items |
cols | number | 2 | Number of columns |
gap | number | 4 | Size of the gap between items in px |
rowHeight | number | 'auto' | 'auto' | Height of grid rows (ignored in masonry layout) |
sx | object | - | The system prop for custom styling |
For ImageListItem, these are the primary props:
Prop | Type | Default | Description |
---|---|---|---|
cols | number | 1 | Number of grid columns the item spans |
rows | number | 1 | Number of grid rows the item spans |
sx | object | - | The system prop for custom styling |
For ImageListItemBar, the main configuration options are:
Prop | Type | Default | Description |
---|---|---|---|
title | node | - | Title to be displayed |
subtitle | node | - | Subtitle to be displayed |
position | 'top' | 'bottom' | 'bottom' | Position of the title bar |
actionIcon | node | - | An IconButton element to be displayed |
actionPosition | 'left' | 'right' | 'right' | Position of the action icon |
Understanding these props gives you the foundation to build customized image galleries tailored to your specific needs.
Setting Up Your Project
Before diving into implementation, let's set up a React project with Material UI installed. If you already have a project, you can skip to the next section.
Creating a New React Project
First, let's create a new React application using Create React App:
npx create-react-app mui-image-gallery
cd mui-image-gallery
Installing Material UI
Next, install Material UI and its dependencies:
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled
Now we're ready to start building our gallery!
Building a Basic ImageList Gallery
Let's start with a simple implementation to understand the core functionality before adding hover effects.
Creating the Image Data
First, we'll create a collection of image data to work with:
// src/data/imageData.js
export const imageData = [
{
img: 'https://images.unsplash.com/photo-1551963831-b3b1ca40c98e',
title: 'Breakfast',
author: '@bkristastucchio',
rows: 2,
cols: 2,
featured: true,
},
{
img: 'https://images.unsplash.com/photo-1551782450-a2132b4ba21d',
title: 'Burger',
author: '@rollelflex_graphy726',
},
{
img: 'https://images.unsplash.com/photo-1522770179533-24471fcdba45',
title: 'Camera',
author: '@helloimnik',
},
{
img: 'https://images.unsplash.com/photo-1444418776041-9c7e33cc5a9c',
title: 'Coffee',
author: '@nolanissac',
cols: 2,
},
{
img: 'https://images.unsplash.com/photo-1533827432537-70133748f5c8',
title: 'Hats',
author: '@hjrc33',
cols: 2,
},
{
img: 'https://images.unsplash.com/photo-1558642452-9d2a7deb7f62',
title: 'Honey',
author: '@arwinneil',
rows: 2,
cols: 2,
featured: true,
},
{
img: 'https://images.unsplash.com/photo-1516802273409-68526ee1bdd6',
title: 'Basketball',
author: '@tjdragotta',
},
{
img: 'https://images.unsplash.com/photo-1518756131217-31eb79b20e8f',
title: 'Fern',
author: '@katie_wasserman',
},
{
img: 'https://images.unsplash.com/photo-1597645587822-e99fa5d45d25',
title: 'Mushrooms',
author: '@silverdalex',
rows: 2,
cols: 2,
},
{
img: 'https://images.unsplash.com/photo-1567306301408-9b74779a11af',
title: 'Tomato basil',
author: '@shelleypauls',
},
{
img: 'https://images.unsplash.com/photo-1471357674240-e1a485acb3e1',
title: 'Sea star',
author: '@peterlaster',
},
{
img: 'https://images.unsplash.com/photo-1589118949245-7d38baf380d6',
title: 'Bike',
author: '@southside_customs',
cols: 2,
},
];
Implementing a Standard ImageList
Now, let's create a basic ImageList component:
// src/components/BasicImageList.jsx
import React from 'react';
import { ImageList, ImageListItem, ImageListItemBar } from '@mui/material';
import { imageData } from '../data/imageData';
const BasicImageList = () => {
return (
<ImageList sx={{ width: '100%', height: 450 }} cols={3} gap={8}>
{imageData.map((item) => (
<ImageListItem key={item.img}>
<img
src={`${item.img}?w=248&fit=crop&auto=format`}
srcSet={`${item.img}?w=248&fit=crop&auto=format&dpr=2 2x`}
alt={item.title}
loading="lazy"
/>
<ImageListItemBar
title={item.title}
subtitle={<span>by: {item.author}</span>}
position="below"
/>
</ImageListItem>
))}
</ImageList>
);
};
export default BasicImageList;
This basic implementation creates a grid with three columns, displaying each image with its title and author information below it. The srcSet
attribute provides higher resolution images for devices with higher pixel density, improving the visual quality on retina displays.
Let's integrate this component into our app:
// src/App.js
import React from 'react';
import { Container, Typography, Box } from '@mui/material';
import BasicImageList from './components/BasicImageList';
function App() {
return (
<Container maxWidth="lg">
<Box sx={{ my: 4 }}>
<Typography variant="h4" component="h1" gutterBottom>
MUI Image Gallery
</Typography>
<BasicImageList />
</Box>
</Container>
);
}
export default App;
This gives us a functional but basic image gallery. Now, let's enhance it with hover effects.
Adding Hover Effects to the Gallery
Hover effects can significantly improve the user experience by providing visual feedback and revealing additional information when users interact with images. Let's implement several hover effect techniques.
1. Basic Zoom Effect on Hover
A simple zoom effect can make your gallery feel more interactive:
// src/components/ZoomImageList.jsx
import React from 'react';
import { ImageList, ImageListItem, ImageListItemBar, Box } from '@mui/material';
import { imageData } from '../data/imageData';
const ZoomImageList = () => {
return (
<ImageList sx={{ width: '100%', height: 450 }} cols={3} gap={8}>
{imageData.map((item) => (
<ImageListItem
key={item.img}
sx={{
overflow: 'hidden',
'& img': {
transition: 'transform 0.3s ease-in-out',
},
'&:hover img': {
transform: 'scale(1.1)',
},
}}
>
<img
src={`${item.img}?w=248&fit=crop&auto=format`}
srcSet={`${item.img}?w=248&fit=crop&auto=format&dpr=2 2x`}
alt={item.title}
loading="lazy"
/>
<ImageListItemBar
title={item.title}
subtitle={<span>by: {item.author}</span>}
position="below"
/>
</ImageListItem>
))}
</ImageList>
);
};
export default ZoomImageList;
This implementation adds a smooth zoom effect when hovering over any image. The sx
prop applies custom styles directly to the component, including CSS transitions for a smoother effect.
2. Fade-in Information Overlay
Let's create a more sophisticated effect where information appears on hover:
// src/components/HoverInfoImageList.jsx
import React from 'react';
import { ImageList, ImageListItem, ImageListItemBar, IconButton, Box } from '@mui/material';
import InfoIcon from '@mui/icons-material/Info';
import { imageData } from '../data/imageData';
const HoverInfoImageList = () => {
return (
<ImageList sx={{ width: '100%', height: 450 }} cols={3} gap={8}>
{imageData.map((item) => (
<ImageListItem
key={item.img}
sx={{
overflow: 'hidden',
'& img': {
transition: 'transform 0.3s ease-in-out',
},
'&:hover img': {
transform: 'scale(1.1)',
},
'& .MuiImageListItemBar-root': {
background: 'linear-gradient(to top, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.3) 70%, rgba(0,0,0,0) 100%)',
transform: 'translateY(100%)',
transition: 'transform 0.3s ease',
opacity: 0,
},
'&:hover .MuiImageListItemBar-root': {
transform: 'translateY(0)',
opacity: 1,
}
}}
>
<img
src={`${item.img}?w=248&fit=crop&auto=format`}
srcSet={`${item.img}?w=248&fit=crop&auto=format&dpr=2 2x`}
alt={item.title}
loading="lazy"
/>
<ImageListItemBar
title={item.title}
subtitle={<span>by: {item.author}</span>}
actionIcon={
<IconButton
sx={{ color: 'white' }}
aria-label={`info about ${item.title}`}
>
<InfoIcon />
</IconButton>
}
/>
</ImageListItem>
))}
</ImageList>
);
};
export default HoverInfoImageList;
In this example, we've created a more complex hover effect where:
- The image scales slightly on hover
- The information bar slides up from the bottom
- The gradient background ensures text remains readable over any image
3. Creating a Quilted Layout with Hover Effects
Let's implement a more advanced gallery with a quilted layout and custom hover effects:
// src/components/QuiltedHoverGallery.jsx
import React from 'react';
import { ImageList, ImageListItem, ImageListItemBar, Box, Typography } from '@mui/material';
import { imageData } from '../data/imageData';
function srcset(image, size, rows = 1, cols = 1) {
return {
src: `${image}?w=${size * cols}&h=${size * rows}&fit=crop&auto=format`,
srcSet: `${image}?w=${size * cols}&h=${size * rows}&fit=crop&auto=format&dpr=2 2x`,
};
}
const QuiltedHoverGallery = () => {
return (
<ImageList
sx={{ width: '100%', height: 500, margin: 0 }}
variant="quilted"
cols={4}
rowHeight={121}
>
{imageData.map((item) => (
<ImageListItem
key={item.img}
cols={item.cols || 1}
rows={item.rows || 1}
sx={{
overflow: 'hidden',
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
opacity: 0,
transition: 'opacity 0.3s ease',
zIndex: 1,
},
'&:hover::before': {
opacity: 1,
},
'& .image-info': {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%) scale(0.8)',
color: 'white',
zIndex: 2,
textAlign: 'center',
opacity: 0,
transition: 'all 0.3s ease',
width: '80%',
},
'&:hover .image-info': {
opacity: 1,
transform: 'translate(-50%, -50%) scale(1)',
},
'& img': {
transition: 'transform 0.5s ease',
},
'&:hover img': {
transform: 'scale(1.1)',
}
}}
>
<img
{...srcset(item.img, 121, item.rows, item.cols)}
alt={item.title}
loading="lazy"
/>
<Box className="image-info">
<Typography variant="h6">{item.title}</Typography>
<Typography variant="body2">{item.author}</Typography>
</Box>
</ImageListItem>
))}
</ImageList>
);
};
export default QuiltedHoverGallery;
This implementation creates a sophisticated quilted layout where:
- Images have different sizes based on their
rows
andcols
properties - A dark overlay appears on hover
- Image information fades in and scales up when hovered
- The image itself zooms slightly for a dynamic effect
The srcset
function helps generate appropriate image URLs based on the item's dimensions, ensuring images load at the right size for their grid position.
Responsive Gallery Implementation
A great gallery should look good on all devices. Let's create a responsive version that adapts to different screen sizes:
// src/components/ResponsiveGallery.jsx
import React from 'react';
import { ImageList, ImageListItem, ImageListItemBar, Box, Typography, useMediaQuery, useTheme } from '@mui/material';
import { imageData } from '../data/imageData';
const ResponsiveGallery = () => {
const theme = useTheme();
const isXs = useMediaQuery(theme.breakpoints.down('sm'));
const isSm = useMediaQuery(theme.breakpoints.between('sm', 'md'));
const isMd = useMediaQuery(theme.breakpoints.between('md', 'lg'));
// Determine columns based on screen size
const getCols = () => {
if (isXs) return 1;
if (isSm) return 2;
if (isMd) return 3;
return 4; // lg and above
};
// Determine row height based on screen size
const getRowHeight = () => {
if (isXs) return 300;
if (isSm) return 200;
return 180;
};
return (
<ImageList
variant="quilted"
cols={getCols()}
rowHeight={getRowHeight()}
sx={{ width: '100%', m: 0 }}
>
{imageData.map((item) => {
// Adjust item size based on screen
// On small screens, make all items single column
const cols = isXs ? 1 : (item.cols || 1);
const rows = isXs ? 1 : (item.rows || 1);
return (
<ImageListItem
key={item.img}
cols={cols}
rows={rows}
sx={{
overflow: 'hidden',
position: 'relative',
'& .overlay': {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.4)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
opacity: 0,
transition: 'opacity 0.3s ease',
zIndex: 1,
},
'&:hover .overlay': {
opacity: 1,
},
'& img': {
transition: 'transform 0.5s ease',
},
'&:hover img': {
transform: 'scale(1.1)',
}
}}
>
<img
src={`${item.img}?w=${getRowHeight() * cols}&h=${getRowHeight() * rows}&fit=crop&auto=format`}
srcSet={`${item.img}?w=${getRowHeight() * cols}&h=${getRowHeight() * rows}&fit=crop&auto=format&dpr=2 2x`}
alt={item.title}
loading="lazy"
/>
<Box className="overlay">
<Box sx={{ textAlign: 'center', color: 'white', p: 2 }}>
<Typography variant={isXs ? 'h5' : 'h6'} component="h3">
{item.title}
</Typography>
<Typography variant="body2">
{item.author}
</Typography>
</Box>
</Box>
{/* Show permanently on mobile, since hover doesn't work well */}
{isXs && (
<ImageListItemBar
title={item.title}
subtitle={item.author}
/>
)}
</ImageListItem>
);
})}
</ImageList>
);
};
export default ResponsiveGallery;
This responsive implementation:
- Uses MUI's
useMediaQuery
hook to detect screen size - Adjusts the number of columns and row height based on viewport width
- Modifies the layout for mobile devices where hover isn't available
- Ensures text is appropriately sized for each screen size
- Maintains the hover effects on desktop while providing alternative UI for touch devices
Advanced Gallery with Custom Hover Transitions
Let's create a more sophisticated gallery with custom transitions and effects:
// src/components/AdvancedHoverGallery.jsx
import React, { useState } from 'react';
import {
ImageList,
ImageListItem,
Box,
Typography,
IconButton,
Fade,
useMediaQuery,
useTheme
} from '@mui/material';
import FavoriteIcon from '@mui/icons-material/Favorite';
import ShareIcon from '@mui/icons-material/Share';
import FullscreenIcon from '@mui/icons-material/Fullscreen';
import { imageData } from '../data/imageData';
import { styled } from '@mui/material/styles';
// Styled components for our gallery
const GalleryImage = styled('img')(({ theme }) => ({
width: '100%',
height: '100%',
objectFit: 'cover',
transition: 'transform 0.5s cubic-bezier(0.4, 0, 0.2, 1)',
}));
const ImageOverlay = styled(Box)(({ theme }) => ({
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'linear-gradient(to top, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.2) 50%, rgba(0,0,0,0.7) 100%)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: theme.spacing(2),
boxSizing: 'border-box',
opacity: 0,
transition: 'opacity 0.4s ease',
}));
const ActionButtons = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
width: '100%',
}));
const ImageTitle = styled(Typography)(({ theme }) => ({
color: 'white',
textShadow: '1px 1px 3px rgba(0,0,0,0.6)',
transform: 'translateY(20px)',
transition: 'transform 0.4s ease',
}));
const ImageAuthor = styled(Typography)(({ theme }) => ({
color: 'white',
textShadow: '1px 1px 3px rgba(0,0,0,0.6)',
transform: 'translateY(20px)',
transition: 'transform 0.4s ease 0.1s', // Slight delay for cascade effect
}));
const ActionButtonContainer = styled(Box)(({ theme }) => ({
transform: 'translateY(-20px)',
transition: 'transform 0.4s ease 0.1s',
}));
const AdvancedHoverGallery = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [hoveredItem, setHoveredItem] = useState(null);
// For mobile, we'll show a different view since hover isn't reliable
const variant = isMobile ? 'masonry' : 'quilted';
const cols = isMobile ? 1 : 4;
const handleMouseEnter = (id) => {
setHoveredItem(id);
};
const handleMouseLeave = () => {
setHoveredItem(null);
};
return (
<ImageList
variant={variant}
cols={cols}
gap={16}
sx={{ width: '100%', height: isMobile ? 'auto' : 600, m: 0 }}
>
{imageData.map((item, index) => (
<ImageListItem
key={item.img}
cols={isMobile ? 1 : (item.cols || 1)}
rows={isMobile ? 1 : (item.rows || 1)}
onMouseEnter={() => handleMouseEnter(index)}
onMouseLeave={handleMouseLeave}
sx={{
overflow: 'hidden',
borderRadius: 1,
boxShadow: '0 4px 10px rgba(0,0,0,0.1)',
position: 'relative',
cursor: 'pointer',
'&:hover img': {
transform: 'scale(1.08)',
},
'&:hover .overlay': {
opacity: 1,
},
'&:hover .title, &:hover .author': {
transform: 'translateY(0)',
},
'&:hover .actions': {
transform: 'translateY(0)',
},
}}
>
<GalleryImage
src={`${item.img}?w=500&fit=crop&auto=format`}
srcSet={`${item.img}?w=500&fit=crop&auto=format&dpr=2 2x`}
alt={item.title}
loading="lazy"
/>
<ImageOverlay className="overlay">
<Box>
<ImageTitle
variant="h6"
component="h3"
className="title"
>
{item.title}
</ImageTitle>
<ImageAuthor
variant="body2"
className="author"
>
{item.author}
</ImageAuthor>
</Box>
<ActionButtonContainer className="actions">
<ActionButtons>
<Box>
<IconButton size="small" sx={{ color: 'white' }}>
<FavoriteIcon />
</IconButton>
<IconButton size="small" sx={{ color: 'white' }}>
<ShareIcon />
</IconButton>
</Box>
<IconButton size="small" sx={{ color: 'white' }}>
<FullscreenIcon />
</IconButton>
</ActionButtons>
</ActionButtonContainer>
</ImageOverlay>
{/* Optional: Add a fade effect when hovering between items */}
<Fade in={hoveredItem !== null && hoveredItem !== index}>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
bgcolor: 'rgba(0,0,0,0.3)',
zIndex: 0,
pointerEvents: 'none',
}}
/>
</Fade>
</ImageListItem>
))}
</ImageList>
);
};
export default AdvancedHoverGallery;
This advanced implementation:
- Uses styled components for better organization and reusability
- Implements staggered animations where text and buttons animate with slight delays
- Adds a subtle fade effect to non-hovered items to create focus
- Includes interactive elements like favorite and share buttons
- Adapts the layout for mobile devices
- Uses different animation curves for a more polished feel
The hover effect in this gallery is more sophisticated, with multiple elements animating independently to create a cohesive and engaging user experience.
Creating a Masonry Gallery with Lazy Loading
For image-heavy applications, performance is crucial. Let's build a masonry gallery with lazy loading:
// src/components/MasonryLazyGallery.jsx
import React, { useState, useEffect, useRef } from 'react';
import { ImageList, ImageListItem, Box, Typography, CircularProgress } from '@mui/material';
import { imageData } from '../data/imageData';
const MasonryLazyGallery = () => {
const [visibleItems, setVisibleItems] = useState(6);
const [loading, setLoading] = useState(false);
const galleryRef = useRef(null);
// Simulate fetching more images when scrolling
const loadMoreItems = () => {
if (loading || visibleItems >= imageData.length) return;
setLoading(true);
// Simulate network delay
setTimeout(() => {
setVisibleItems(prev => Math.min(prev + 6, imageData.length));
setLoading(false);
}, 1000);
};
// Set up intersection observer for infinite scrolling
useEffect(() => {
const options = {
root: null,
rootMargin: '0px',
threshold: 0.1
};
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !loading) {
loadMoreItems();
}
}, options);
if (galleryRef.current) {
observer.observe(galleryRef.current);
}
return () => {
if (galleryRef.current) {
observer.unobserve(galleryRef.current);
}
};
}, [loading, visibleItems]);
return (
<Box sx={{ width: '100%', overflow: 'hidden' }}>
<ImageList variant="masonry" cols={3} gap={8}>
{imageData.slice(0, visibleItems).map((item) => (
<ImageListItem
key={item.img}
sx={{
overflow: 'hidden',
borderRadius: 1,
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
'&:hover': {
transform: 'translateY(-5px)',
boxShadow: '0 10px 20px rgba(0,0,0,0.2)',
'& .image-info': {
opacity: 1,
transform: 'translateY(0)',
},
'& img': {
transform: 'scale(1.05)',
},
},
position: 'relative',
}}
>
<img
src={`${item.img}?w=248&fit=crop&auto=format`}
srcSet={`${item.img}?w=248&fit=crop&auto=format&dpr=2 2x`}
alt={item.title}
loading="lazy"
style={{
transition: 'transform 0.5s ease',
}}
/>
<Box
className="image-info"
sx={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
bgcolor: 'rgba(0,0,0,0.7)',
color: 'white',
padding: 1,
transform: 'translateY(100%)',
opacity: 0,
transition: 'all 0.3s ease',
}}
>
<Typography variant="subtitle1">{item.title}</Typography>
<Typography variant="body2">{item.author}</Typography>
</Box>
</ImageListItem>
))}
</ImageList>
{/* Loading indicator and sentinel element for intersection observer */}
<Box
ref={galleryRef}
sx={{
display: 'flex',
justifyContent: 'center',
padding: 2,
visibility: visibleItems >= imageData.length ? 'hidden' : 'visible'
}}
>
{loading && <CircularProgress />}
</Box>
</Box>
);
};
export default MasonryLazyGallery;
This implementation:
- Uses the masonry layout which works well for images of varying heights
- Implements lazy loading using the Intersection Observer API
- Only loads a small batch of images initially, then loads more as the user scrolls
- Shows a loading indicator while fetching more images
- Includes a subtle hover effect that lifts the image and reveals information
- Uses the
loading="lazy"
attribute for browser-level image lazy loading
This approach significantly improves performance for galleries with many images by:
- Reducing initial load time
- Decreasing memory usage
- Minimizing network traffic
- Improving perceived performance with visual feedback
Customizing ImageList with the Theme Provider
For consistent styling across your application, you can customize the ImageList component using MUI's theming system:
// src/theme.js
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
components: {
MuiImageList: {
styleOverrides: {
root: {
// Default styles for all ImageList components
margin: 0,
padding: 0,
},
},
variants: [
{
props: { variant: 'custom-gallery' },
style: {
gap: 16,
borderRadius: 8,
overflow: 'hidden',
},
},
],
},
MuiImageListItem: {
styleOverrides: {
root: {
// Default styles for all ImageListItem components
overflow: 'hidden',
borderRadius: 4,
},
},
},
MuiImageListItemBar: {
styleOverrides: {
root: {
background: 'linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.4) 70%, rgba(0,0,0,0) 100%)',
},
title: {
fontSize: '1rem',
fontWeight: 500,
},
subtitle: {
fontSize: '0.75rem',
},
},
},
},
});
export default theme;
Then apply the theme to your application:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import App from './App';
import theme from './theme';
ReactDOM.render(
<React.StrictMode>
<ThemeProvider theme={theme}>
<CssBaseline />
<App />
</ThemeProvider>
</React.StrictMode>,
document.getElementById('root')
);
Now you can create a themed gallery component:
// src/components/ThemedGallery.jsx
import React from 'react';
import { ImageList, ImageListItem, ImageListItemBar, Box } from '@mui/material';
import { imageData } from '../data/imageData';
const ThemedGallery = () => {
return (
<ImageList
variant="custom-gallery" // Our custom variant defined in the theme
cols={3}
gap={8}
sx={{ width: '100%', height: 450 }}
>
{imageData.map((item) => (
<ImageListItem
key={item.img}
sx={{
'&:hover img': {
transform: 'scale(1.1)',
},
'& img': {
transition: 'transform 0.3s ease',
},
}}
>
<img
src={`${item.img}?w=248&fit=crop&auto=format`}
srcSet={`${item.img}?w=248&fit=crop&auto=format&dpr=2 2x`}
alt={item.title}
loading="lazy"
/>
<ImageListItemBar
title={item.title}
subtitle={item.author}
/>
</ImageListItem>
))}
</ImageList>
);
};
export default ThemedGallery;
This approach:
- Centralizes styling in your theme
- Ensures consistent styling across your application
- Makes it easier to implement design changes globally
- Allows for custom variants that can be reused
Accessibility Considerations
Creating an accessible image gallery is essential for users with disabilities. Here's how to enhance the accessibility of your MUI ImageList:
// src/components/AccessibleGallery.jsx
import React from 'react';
import {
ImageList,
ImageListItem,
ImageListItemBar,
IconButton,
Box,
Typography,
VisuallyHidden
} from '@mui/material';
import InfoIcon from '@mui/icons-material/Info';
import { imageData } from '../data/imageData';
// Custom component for screen readers
const VisuallyHidden = ({ children }) => (
<Box
sx={{
border: 0,
clip: 'rect(0 0 0 0)',
height: '1px',
margin: '-1px',
overflow: 'hidden',
padding: 0,
position: 'absolute',
width: '1px',
}}
>
{children}
</Box>
);
const AccessibleGallery = () => {
return (
<>
{/* Screen reader heading */}
<Typography variant="h2" component="h2" id="gallery-heading">
Photo Gallery
</Typography>
<ImageList
sx={{ width: '100%', height: 450 }}
cols={3}
gap={8}
// Connect to the heading with aria-labelledby
aria-labelledby="gallery-heading"
// Identify as a gallery for screen readers
role="group"
>
{imageData.map((item, index) => (
<ImageListItem
key={item.img}
// Make items focusable with keyboard
tabIndex={0}
// Add keyboard event handling
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
// Handle selection (e.g., open modal)
alert(`Selected: ${item.title}`);
e.preventDefault();
}
}}
sx={{
overflow: 'hidden',
'&:focus-visible': {
outline: '3px solid #1976d2',
outlineOffset: '2px',
},
'&:hover img, &:focus img': {
transform: 'scale(1.1)',
},
'& img': {
transition: 'transform 0.3s ease',
},
}}
>
<img
src={`${item.img}?w=248&fit=crop&auto=format`}
srcSet={`${item.img}?w=248&fit=crop&auto=format&dpr=2 2x`}
alt={item.title} // Meaningful alt text
loading="lazy"
// Improved description for screen readers
aria-describedby={`desc-${index}`}
/>
<VisuallyHidden>
<span id={`desc-${index}`}>
{item.title} by {item.author}. Image {index + 1} of {imageData.length}.
</span>
</VisuallyHidden>
<ImageListItemBar
title={item.title}
subtitle={<span>by: {item.author}</span>}
actionIcon={
<IconButton
sx={{ color: 'white' }}
aria-label={`more information about ${item.title}`}
>
<InfoIcon />
</IconButton>
}
/>
</ImageListItem>
))}
</ImageList>
</>
);
};
export default AccessibleGallery;
This implementation enhances accessibility by:
- Adding proper ARIA attributes to identify the gallery
- Ensuring keyboard navigation works with tabIndex and keyboard event handlers
- Providing visible focus indicators for keyboard users
- Including descriptive alt text for images
- Adding additional context for screen readers with visually hidden text
- Making interactive elements like buttons accessible with aria-label
These improvements ensure that users with disabilities, including those using screen readers or keyboard navigation, can effectively interact with your gallery.
Performance Optimization Techniques
For large galleries, performance can become an issue. Here are some optimization techniques:
// src/components/OptimizedGallery.jsx
import React, { useState, useCallback, useMemo } from 'react';
import { ImageList, ImageListItem, ImageListItemBar, Box } from '@mui/material';
import { imageData } from '../data/imageData';
import { useInView } from 'react-intersection-observer';
// Memoized image component to prevent unnecessary re-renders
const MemoizedImage = React.memo(({ item, inView }) => {
// Only render the actual image when it's in view
return (
<>
{inView ? (
<img
src={`${item.img}?w=248&fit=crop&auto=format`}
srcSet={`${item.img}?w=248&fit=crop&auto=format&dpr=2 2x`}
alt={item.title}
loading="lazy"
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
) : (
// Placeholder with correct dimensions to prevent layout shifts
<Box
sx={{
width: '100%',
height: '100%',
bgcolor: 'grey.200',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
Loading...
</Box>
)}
</>
);
});
const OptimizedGallery = () => {
// Memoize the filtered data to prevent recalculations
const displayData = useMemo(() => {
return imageData.slice(0, 50); // Limit initial load
}, []);
return (
<ImageList
variant="masonry"
cols={3}
gap={8}
sx={{ width: '100%', height: 'auto', m: 0 }}
>
{displayData.map((item, index) => {
// Use intersection observer to track visibility
const [ref, inView] = useInView({
triggerOnce: true,
rootMargin: '200px 0px', // Load images 200px before they come into view
});
return (
<ImageListItem
ref={ref}
key={item.img}
sx={{
overflow: 'hidden',
'&:hover img': {
transform: 'scale(1.1)',
},
'& img': {
transition: 'transform 0.3s ease',
},
}}
>
<MemoizedImage item={item} inView={inView} />
{inView && (
<ImageListItemBar
title={item.title}
subtitle={item.author}
/>
)}
</ImageListItem>
);
})}
</ImageList>
);
};
export default OptimizedGallery;
This optimized implementation:
- Uses React.memo to prevent unnecessary re-renders of image components
- Implements true lazy loading with IntersectionObserver via the react-intersection-observer library
- Only renders the actual image content when it's close to the viewport
- Uses placeholders to maintain layout stability
- Limits the initial number of images rendered
- Conditionally renders ImageListItemBar components only when needed
These optimizations significantly improve performance for large galleries by:
- Reducing initial render time
- Decreasing memory usage
- Minimizing unnecessary DOM operations
- Preventing layout shifts during loading
Common Issues and Solutions
When working with MUI ImageList, you might encounter these common issues:
1. Images with Different Aspect Ratios
Problem: Images with different aspect ratios can create an inconsistent layout.
Solution: Use the masonry layout or manually control dimensions:
// Solution for consistent image heights in standard layout
<ImageList cols={3} rowHeight={200}>
{imageData.map((item) => (
<ImageListItem key={item.img}>
<img
src={`${item.img}?w=248&h=200&fit=crop&auto=format`}
alt={item.title}
style={{ height: 200, objectFit: 'cover' }}
/>
</ImageListItem>
))}
</ImageList>
// Alternative: Use masonry layout
<ImageList variant="masonry" cols={3} gap={8}>
{imageData.map((item) => (
<ImageListItem key={item.img}>
<img
src={`${item.img}?w=248&auto=format`}
alt={item.title}
/>
</ImageListItem>
))}
</ImageList>
2. Performance with Many Images
Problem: Loading many high-resolution images can impact performance.
Solution: Implement windowing with react-window:
// Using react-window for virtualized rendering
import React from 'react';
import { FixedSizeGrid } from 'react-window';
import { Box } from '@mui/material';
import { imageData } from '../data/imageData';
const VirtualizedGallery = () => {
const COLUMN_COUNT = 3;
const ITEM_WIDTH = 300;
const ITEM_HEIGHT = 200;
const GAP = 8;
// Calculate rows based on data length and column count
const ROW_COUNT = Math.ceil(imageData.length / COLUMN_COUNT);
// Render a cell with an image
const Cell = ({ columnIndex, rowIndex, style }) => {
const index = rowIndex * COLUMN_COUNT + columnIndex;
if (index >= imageData.length) return null;
const item = imageData[index];
// Adjust style to account for gap
const adjustedStyle = {
...style,
left: `${parseFloat(style.left) + GAP * columnIndex}px`,
top: `${parseFloat(style.top) + GAP * rowIndex}px`,
width: `${parseFloat(style.width) - GAP}px`,
height: `${parseFloat(style.height) - GAP}px`,
};
return (
<Box style={adjustedStyle}>
<img
src={`${item.img}?w=${ITEM_WIDTH}&h=${ITEM_HEIGHT}&fit=crop&auto=format`}
alt={item.title}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</Box>
);
};
return (
<FixedSizeGrid
columnCount={COLUMN_COUNT}
columnWidth={ITEM_WIDTH + GAP}
rowCount={ROW_COUNT}
rowHeight={ITEM_HEIGHT + GAP}
height={600}
width={COLUMN_COUNT * (ITEM_WIDTH + GAP) - GAP}
>
{Cell}
</FixedSizeGrid>
);
};
export default VirtualizedGallery;
3. Flickering Hover Effects
Problem: Hover effects may flicker when moving the cursor rapidly.
Solution: Use CSS transitions with a slight delay:
// Preventing flickering hover effects
<ImageListItem
sx={{
'& .overlay': {
opacity: 0,
transition: 'opacity 0.3s ease 0.1s', // Added small delay
},
'&:hover .overlay': {
opacity: 1,
},
}}
>
<img src={item.img} alt={item.title} />
<Box className="overlay">
{/* Overlay content */}
</Box>
</ImageListItem>
4. Accessibility for Keyboard Users
Problem: Hover effects don't work for keyboard users.
Solution: Add focus styles that match hover effects:
// Making hover effects work for keyboard users
<ImageListItem
tabIndex={0} // Make focusable
sx={{
'&:hover .overlay, &:focus .overlay': { // Added focus selector
opacity: 1,
},
'&:focus': {
outline: '2px solid #1976d2',
outlineOffset: '2px',
},
}}
>
<img src={item.img} alt={item.title} />
<Box className="overlay">
{/* Overlay content */}
</Box>
</ImageListItem>
5. Mobile Touch Support
Problem: Hover effects don't work on touch devices.
Solution: Implement click/touch toggles for mobile:
// Touch-friendly gallery with toggleable overlays
import React, { useState } from 'react';
import { ImageList, ImageListItem, Box, useMediaQuery, useTheme } from '@mui/material';
import { imageData } from '../data/imageData';
const TouchFriendlyGallery = () => {
const [activeItem, setActiveItem] = useState(null);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const handleItemClick = (index) => {
if (isMobile) {
setActiveItem(activeItem === index ? null : index);
}
};
return (
<ImageList cols={isMobile ? 1 : 3} gap={8}>
{imageData.map((item, index) => (
<ImageListItem
key={item.img}
onClick={() => handleItemClick(index)}
sx={{
cursor: 'pointer',
'& .overlay': {
opacity: isMobile
? (activeItem === index ? 1 : 0) // For mobile: show on click
: 0, // For desktop: initially hidden
transition: 'opacity 0.3s ease',
},
'&:hover .overlay': {
opacity: isMobile ? (activeItem === index ? 1 : 0) : 1, // Only apply hover on desktop
},
}}
>
<img src={item.img} alt={item.title} />
<Box className="overlay">
{/* Overlay content */}
</Box>
</ImageListItem>
))}
</ImageList>
);
};
export default TouchFriendlyGallery;
Wrapping Up
In this comprehensive guide, we've explored how to create engaging image galleries with MUI's ImageList component and custom hover effects. We've covered everything from basic implementation to advanced techniques, including responsive design, accessibility considerations, and performance optimizations.
The MUI ImageList component provides a powerful foundation for displaying image collections, while custom hover effects add interactivity and visual appeal. By combining these elements with proper accessibility practices and performance optimizations, you can create galleries that are both beautiful and functional for all users.
Remember that the best gallery implementation depends on your specific needs and content. Experiment with different layouts, hover effects, and customization options to find the perfect solution for your project.