Image Carousel With Zoom Using Nextjs And Material UI

Photo by Salma Smida: https://www.pexels.com/photo/carousel-at-the-park-1375016/

Introduction

An image carousel is a great way to showcase a collection of images on your website. By adding the ability to zoom in on each image, you can give users a closer look and provide a more engaging experience. This blog will go through the steps of building an image carousel with zoom functionality in Next.js. Whether you are a beginner or an experienced web developer, this guide will provide the knowledge and practical examples to help you create a compelling image carousel with zoom. So let’s get started and make your website images pop!

The example code generates a carousel with zoom using the Pure-React-Carousel. Out of the box Pure-React-Carousel come with a nice hover zoom feature and basic features that are easy to customize. The two main customizations are the Next & Previous buttons and a vertical thumbnail section with a glowing border on the selected image.

Image Resource

Images for this example were taken from Lorem Picsum Photos.

Images are locate under ‘.\public\images’

Github Repository with Solution

GitHub

Prerequisites

If you are not using the source files above, get the Next.js template from MUI. You might have to clone the entire repository to get it, but it is worth the effort. The template from MUI has everything configured out of the box and is ready to use. If you are installing Material UI yourself, it can be challenging unless you are comfortable with Next.js.

https://github.com/mui/material-ui/tree/master/examples/nextjs

MUI – Material UI v5.4.3

Material UI library for React

npm install @mui/material @mui/lab @emotion/react @emotion/styled

Pure-React-Carousel library. This library is structured so the developer can easily customize any component without trying to work around strict CSS or DOM rules.

npm install pure-react-carousel

NextJS Configuration (Optional)

tsconfig.json – Configuration for folder locations under compilerOptions.

So you can use ‘@/components/’ instead of ‘../../components/’

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
        "@/components/*": ["components/*"],
        "@/src/*": ["src/*"],
        "@/styles/*": ["styles/*"],
        "@/store/*": ["store/*"]
    },
...

Image Carousel Component CSS Styline

CSS for the ImageCarousel.css file. The buttonBack and buttonNext style the plain out of the box next buttons so they look like transparent Fab buttons with an arrow symbol.

 
  .buttonBack {
    position: absolute;
    z-index: 999;
    width: 43px;
    height: 43px;
    background: #FAFAFA;
    border: 1px;
    border-radius: 100%;
    color: grey;
    opacity: 0.40;
    cursor: pointer;
    display: flex;
    padding-left: 15px;
    justify-content: center;
    align-items: center;      
    top: 50%;
    left: 0;
    transform: translateY(-50%);
  }
  .buttonBack:hover {
    opacity: 1;
  }
  
  .buttonNext {
    position: absolute;
    z-index: 999;
    width: 43px;
    height: 43px;
    background: #FAFAFA;
    border: 1px;
    border-radius: 100%;
    color: grey;
    opacity: 0.40;
    cursor: pointer;
    display: flex;
    padding-left: 7px;
    justify-content: center;
    align-items: center;      
    
    top: 50%;
    right: 0;
    transform: translateY(-50%);

  }
  .buttonNext:hover {
    opacity: 1;
  }

Thumbnail glow animation.

  .glow {
    
    border: solid;
    border-color: #1c87c9;        
    cursor: pointer;
    display: inline-block;
    font-family: sans-serif;
    font-size: 20px;
    height: 100%;
    text-align: center;
    text-decoration: none;
  }

  @keyframes glowing {
    0% {
      border-color: #1c87c9;        
      box-shadow: 0 0 3px #1c87c9;
    }
    50% {
      border-color: #1c87c9;        
      box-shadow: 0 0 10px #1c87c9;
    }
    100% {
      border-color: #1c87c9;        
      box-shadow: 0 0 3px #1c87c9;
    }
  }

  .glow {
    animation: glowing 1300ms infinite;
  }

The complete CSS file.

All together

.text {
    background-color: #04AA6D;
    color: white;
    font-size: 16px;
    padding: 16px 32px;
  }

  .container {
    position: relative;
    max-width: 800px;
    margin: auto;
  }
  
  .buttonBack {
    position: absolute;
    z-index: 999;
    width: 43px;
    height: 43px;
    background: #FAFAFA;
    border: 1px;
    border-radius: 100%;
    color: grey;
    opacity: 0.40;
    cursor: pointer;
    display: flex;
    padding-left: 15px;
    justify-content: center;
    align-items: center;      
    top: 50%;
    left: 0;
    transform: translateY(-50%);
  }
  .buttonBack:hover {
    opacity: 1;
  }
  
  .buttonNext {
    position: absolute;
    z-index: 999;
    width: 43px;
    height: 43px;
    background: #FAFAFA;
    border: 1px;
    border-radius: 100%;
    color: grey;
    opacity: 0.40;
    cursor: pointer;
    display: flex;
    padding-left: 7px;
    justify-content: center;
    align-items: center;      
    
    top: 50%;
    right: 0;
    transform: translateY(-50%);

  }
  .buttonNext:hover {
    opacity: 1;
  }
  
  
  .dotGroup {
    text-align: center;
  }

  .glow {
    
    border: solid;
    border-color: #1c87c9;        
    cursor: pointer;
    display: inline-block;
    font-family: sans-serif;
    font-size: 20px;
    height: 100%;
    text-align: center;
    text-decoration: none;
  }

  @keyframes glowing {
    0% {
      border-color: #1c87c9;        
      box-shadow: 0 0 3px #1c87c9;
    }
    50% {
      border-color: #1c87c9;        
      box-shadow: 0 0 10px #1c87c9;
    }
    100% {
      border-color: #1c87c9;        
      box-shadow: 0 0 3px #1c87c9;
    }
  }
  .glow {
    animation: glowing 1300ms infinite;
  }

Image Carousel Component

Imports and Handling State

Below are the libraries and styles imported for the component. Most of the logic in this component is for the vertically stacked thumbnails. The currentSlide state variable keeps track of the current slide for the thumbnails. The Carousel has it’s own internal way of tracking the current slide. Also the image data will be passed to this as a property labeled images from the parent component. Two events are handled for the previous and next slide to highlight the current thumbnail.

import React, {  useState } from 'react';
...


export default function ImageCarousel(props) {

  const [currentSlide, setCurrentSlide] = useState(0);
  const [imageData, setImageData] = useState(props.images);

  const handleClickPreviousEvent = (event) => {
    setCurrentSlide(currentSlide - 1)
    if(currentSlide <= 0) {
        setCurrentSlide(imageData.length - 1)
    }
  }

  const handleClickNextEvent = (event) => {
    setCurrentSlide(currentSlide + 1)
    if(currentSlide >= imageData.length - 1) {
        setCurrentSlide(0)
    }
  }

Thumbnails

The Carousel is within a container and displays the vertically stacked thumbnails on the left side. When the slide changes from the Next and Previous buttons, it will highlight the corresponding thumbnail. The styles were created in the imageCarousel.module.css. However, if you slide the image it won’t update the thumbnail. Maybe I will add that in a future update.

<Stack direction="column" spacing={2} sx={{ maxHeight: 60 }} >
{props.images.map((item, index) => (
  <Box sx={{ maxWidth: 50, maxHeight: 40, cursor: 'pointer' }}  onClick={() => setCurrentSlide(index)}>
    <Image src={item} width="50" height="50" className={index===currentSlide && compStyling.glow}/>
  </Box>
))}            
</Stack>

Carousel

There are two buttons that were styled in the CSS file imageCarousel.css. These are the Previous and Next buttons that show a left and right chevron/arrow icons. The buttons will have the opacity set so it looks partially transparent and will be fully rendered when a mouse is hovering over it. The carousel naturalSlideHeight and naturalSlideWidth are set to 200 for a squared image view since the images are 1200×1200.

<CarouselProvider
  visibleSlides={1}
  totalSlides={props.images.length}
  currentSlide={currentSlide}
  step={2}
  naturalSlideWidth={200}
  naturalSlideHeight={200}
  hasMasterSpinner
  infinite            
>
<div className={compStyling.container}>       
  <Slider className={compStyling.slider}>

  {props.images.map((item, index) => (                
      <Slide key={index}>
      
          <ImageWithZoom  src={item} key={index}/>
      
      </Slide>
                    
    ))}            
  </Slider>
  <ButtonBack className={compStyling.buttonBack} onClick={(e) => handleClickPreviousEvent(e)}><ArrowBackIosIcon/></ButtonBack>
  <ButtonNext className={compStyling.buttonNext}  onClick={(e) => handleClickNextEvent(e)}> <ArrowForwardIosIcon/></ButtonNext>
  </div>
  {/* <DotGroup dotNumbers /> */}
  
</CarouselProvider>

All Together

Source code for the component

import React, {  useState } from 'react';
import { Container } from "@mui/system"
import { CarouselProvider, Slider, Slide, ImageWithZoom, DotGroup,   ButtonBack, ButtonNext  } from 'pure-react-carousel';
import 'pure-react-carousel/dist/react-carousel.es.css';
import Stack from '@mui/material/Stack';
import Grid from '@mui/material/Grid';
import { Box } from '@mui/material';
import Image from 'next/image';
import compStyling from './ImageCarousel.module.css'
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';


export default function ImageCarousel(props) {

  const [currentSlide, setCurrentSlide] = useState(0);
  const [imageData, setImageData] = useState(props.images);

  const handleClickPreviousEvent = (event) => {
    setCurrentSlide(currentSlide - 1)
    if(currentSlide <= 0) {
        setCurrentSlide(imageData.length - 1)
    }
  }

  const handleClickNextEvent = (event) => {
    setCurrentSlide(currentSlide + 1)
    if(currentSlide >= imageData.length - 1) {
        setCurrentSlide(0)
    }
  }

  return (
    <Container maxWidth='sm'>
      <Grid container spacing={2}>
         <Grid item xs={2}>

           <Stack direction="column" spacing={2} sx={{ maxHeight: 60 }} >
            {props.images.map((item, index) => (
              <Box sx={{ maxWidth: 50, maxHeight: 40, cursor: 'pointer' }}  onClick={() => setCurrentSlide(index)}>
                <Image src={item} width="50" height="50" className={index===currentSlide && compStyling.glow}/>
              </Box>
            ))}            
          </Stack>

         </Grid>
         <Grid item xs={10}>
         
          <CarouselProvider
            visibleSlides={1}
            totalSlides={props.images.length}
            currentSlide={currentSlide}
            step={2}
            naturalSlideWidth={200}
            naturalSlideHeight={300}
            hasMasterSpinner
            infinite            
          >
          <div className={compStyling.container}>       
            <Slider className={compStyling.slider}>

            {props.images.map((item, index) => (                
                <Slide key={index}>
                
                   <ImageWithZoom  src={item} key={index}/>
                
                </Slide>
                              
              ))}            
            </Slider>
            <ButtonBack className={compStyling.buttonBack} onClick={(e) => handleClickPreviousEvent(e)}><ArrowBackIosIcon/></ButtonBack>
            <ButtonNext className={compStyling.buttonNext}  onClick={(e) => handleClickNextEvent(e)}> <ArrowForwardIosIcon/></ButtonNext>
            </div>
            {/* <DotGroup dotNumbers /> */}
            
         </CarouselProvider>
       
        </Grid>
      </Grid>
    </Container>
  )
}

Image Page

This is the NextJS Page that renders ImageCarousel.js. A hard coded array of images imgArr is sent to the ImageCarousel component.

<ImageCarousel images={imgArr} />

import React from 'react';
import { Container } from "@mui/system"

import ImageCarousel from '@/components/imageCarousel';

export default function Index() {

  return (
    <Container maxWidth='sm'>
      <ImageCarousel images={imgArr}/>
    </Container>
  )
}

const imgArr = ['/images/image_1.jpg', 
'/images/image_2.jpg', 
'/images/image_3.jpg', 
'/images/image_4.jpg', 
'/images/image_5.jpg', 
'/images/image_6.jpg']

Conclusion

In conclusion, building an image carousel with zoom functionality in Next.js can significantly enhance your website’s visual appeal and user experience. Creating an image carousel with zoom can be accomplished with the steps outlined in this blog and is easily customized to fit your specific needs. By incorporating this image carousel with zoom into your Next.js projects, you can provide your users with a more engaging and interactive experience. This is the third carousel article I have written and the other libraries have their advantages, but Pure-React-Carousel was the easiest to customize. So go ahead and put your newfound knowledge to work and make your website images shine!