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 between1.43.0-rc.14
and1.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?