How to Implement Product Image Slider?

To enhance user experience and optimize site performance, a dynamic preview feature has been implemented for product images on the listing page.

This feature divides the main product image into three transparent, invisible zones that cover the entire image.

  • When the user moves the mouse cursor over the left section of the product image, the primary (first) image remains visible.

  • As the cursor transitions toward the center section, the second image is displayed.

  • Similarly, hovering over the right section reveals the third image.

This dynamic behavior ensures that the image corresponding to the cursor’s location is displayed seamlessly. When the cursor exits the product image, the default (first) image is automatically restored.

The number of invisible zones adapts dynamically based on the availability of product images:

  • For products with two images: The visual preview consists of two zones, allowing the same smooth transition between the images.

  • For products with a single image: This feature is not activated, and the primary image remains static.

  • For products with more than three images: The first three images are displayed using this dynamic preview, following the aforementioned logic.

This innovative functionality allows users to quickly explore multiple product images without navigating to the product detail page, significantly improving browsing efficiency while maintaining optimal site performance.

Technical Requirements and Prerequisites​

  • Compatibility: The feature is supported in @akinon/next versions between 1.43.0-rc.14 and 1.72.0.

  • Dependencies: The swiper package is required. Recommended version: 8.4.5.

Implementation Steps​

1. Create the Product Image Slider Component​

  • Navigate to the following folder: apps/projectzeronext/src/views/product-item

  • Create a file named product-image-slider.tsx.

  • Add the following code to the file to implement the slider:

import { Image } from '@akinon/next/components/image';
import { Link, SwiperSlide, SwiperReact } from '@theme/components';
import clsx from 'clsx';
import { useEffect, useRef, useState } from 'react';
import { useInView } from 'react-intersection-observer';
import { Lazy, Virtual } from 'swiper';

const loadImage = (slide) => {
  const image = slide.querySelector('img');
  if (image) {
    const src = image.getAttribute('data-src');
    if (src) {
      image.setAttribute('src', src);
      image.removeAttribute('data-src');
    }
  }
};
const handleSlideChange = (swiper, paginationRef) => {
  const activeIndex = swiper?.activeIndex;
  const bullets = paginationRef?.current?.querySelectorAll(
    '.swiper-pagination-rectangle'
  );

  // Active Slide Show
  if (swiper?.slides[activeIndex]) {
    loadImage(swiper.slides[activeIndex]);
  }

  // Pagination Marks
  bullets?.forEach((bullet, index) => {
    bullet.classList.toggle('!bg-secondary', index === swiper.realIndex);
  });
};

const Slide = ({
  imagesArray,
  swiper,
  swiperRef,
  setSwiper,
  productImage,
  paginationRef,
  SlideItem
}) => {
  useEffect(() => {
    const swiperInstance = swiperRef.current?.swiper;

    if (swiperInstance) {
      // First image is render
      loadImage(swiperInstance.slides[0]);

      swiperInstance.on('slideChange', () =>
        handleSlideChange(swiperInstance, paginationRef)
      );

      return () => {
        swiperInstance.off('slideChange', () =>
          handleSlideChange(swiperInstance, paginationRef)
        );
      };
    }
  }, [swiperRef, paginationRef]);
  return (
    <SwiperReact
      ref={swiperRef}
      slidesPerView={1}
      pagination={{ el: '.swiper-pagination', clickable: true }}
      preloadImages={false}
      lazy={{
        enabled: true,
        checkInView: true,
        loadPrevNext: true,
        loadPrevNextAmount: 0,
        loadOnTransitionStart: true
      }}
      onSwiper={setSwiper}
      modules={[Virtual, Lazy]}
    >
      {imagesArray?.map((item, productImageIndex) =>
        SlideItem(
          productImageIndex === 0 ? productImage : item.image,
          productImageIndex
        )
      )}
      <div
        className="swiper-pagination flex-row items-center justify-center gap-x-1 z-20 absolute bottom-0 w-full"
        ref={paginationRef}
      >
        {imagesArray?.map((item, productImageIndex) => (
          <span
            key={productImageIndex}
            className={clsx(
              productImageIndex === 0 && '!bg-secondary',
              'swiper-pagination-rectangle h-1 w-full bg-gray-650'
            )}
          ></span>
        ))}
      </div>
      <div className="hidden md:flex absolute inset-0 z-10 justify-between">
        {imagesArray.map((image, productImageIndex) => (
          <div
            key={productImageIndex}
            className="w-full"
            onMouseMove={() => swiper?.slideTo(productImageIndex)}
            onMouseEnter={() => {
              paginationRef?.current?.classList.add('flex');
            }}
            onMouseLeave={() => {
              paginationRef?.current?.classList.remove('flex');
            }}
          ></div>
        ))}
      </div>
    </SwiperReact>
  );
};

const SlideImage = ({ image_url, product_name }) => {
  const { ref, inView } = useInView({
    triggerOnce: true
  });
  const default_image = '/default-no-image.png';
  image_url = image_url || default_image;
  return (
    <div ref={ref}>
      <Image
        fill
        loading="lazy"
        priority={false}
        src={inView ? image_url : '/white-bg.png'}
        alt={product_name}
        aspectRatio={1}
        quality={100}
        sizes="
      (max-width: 768px) 175px,
      (max-width: 1170px) 350px,
      500px"
        crop="center"
        className="flex"
        style={{
          imageRendering: '-webkit-optimize-contrast'
        }}
      />
    </div>
  );
};

const ProductImageSlider = ({
  absolute_url,
  multiImages,
  productImages,
  setSwiper,
  productImage,
  swiper,
  image_url_one,
  product_name,
  setViewProducts,
  product,
  index
}) => {
  const swiperRef = useRef(null);
  const paginationRef = useRef(null);
  const [viewed, setViewed] = useState(false);
  const { ref, inView } = useInView();

  const SlideItem = (image_url: string, productImageIndex: number) => (
    <SwiperSlide key={productImageIndex}>
      <SlideImage image_url={image_url} product_name={product_name} />
    </SwiperSlide>
  );

  useEffect(() => {
    const swiper = swiperRef?.current?.swiper;

    // Load Image
    swiper?.slides?.forEach(loadImage);

    // Change Slide Image
    swiper?.on('slideChange', () => handleSlideChange(swiper, paginationRef));

    // Pagination referance
    if (swiper && swiper?.pagination && swiper?.pagination?.el) {
      paginationRef.current = swiper.pagination.el;
    }

    if (!viewed && inView) {
      setViewed(true);
      setViewProducts &&
        setViewProducts((prev) => [...prev, { ...product, index }]);
    }

    // Cleanup function
    return () => {
      if (swiper) {
        swiper?.off('slideChange', () =>
          handleSlideChange(swiper, paginationRef)
        );
      }
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [inView, swiper]);

  return (
    <div ref={ref}>
      <Link href={absolute_url}>
        {!multiImages ? (
          productImages?.length > 1 ? (
            <Slide
              imagesArray={productImages}
              swiper={swiper}
              swiperRef={swiperRef}
              setSwiper={setSwiper}
              productImage={productImage}
              paginationRef={paginationRef}
              SlideItem={SlideItem}
            />
          ) : (
            <SlideImage image_url={image_url_one} product_name={product_name} />
          )
        ) : multiImages?.length > 1 ? (
          <Slide
            imagesArray={multiImages}
            swiper={swiper}
            swiperRef={swiperRef}
            setSwiper={setSwiper}
            productImage={productImage}
            paginationRef={paginationRef}
            SlideItem={SlideItem}
          />
        ) : (
          <SlideImage image_url={image_url_one} product_name={product_name} />
        )}
      </Link>
    </div>
  );
};

export default ProductImageSlider;

This component:

  • Creates a slider with a maximum of three images if the product has multiple visuals.

  • Enables navigation between images when hovering over the product image.

2. Add Swiper Components​

If the Swiper components (swiper-react and swiper-slide) are not available in your project, add them as follows:

Create the swiper-react Component​

  • Navigate to apps/projectzeronext/src/components.

  • Create a file named swiper-react.tsx.

  • Add the following code:

'use client';

import 'swiper/swiper-bundle.min.css';

export { Swiper as SwiperReact } from 'swiper/react';

Create the swiper-slide Component​

  • Navigate to apps/projectzeronext/src/components.

  • Create a file named swiper-slide.tsx.

  • Add the following code:

'use client';

export { SwiperSlide } from 'swiper/react';

3. Update components/index.tsx​

To make the newly created components available across the project:

  • Open the apps/projectzeronext/src/components/index.ts file.

  • Add the following exports:

// Swiper Components
export * from './swiper-react';
export * from './swiper-slide';

Last updated

Was this helpful?