Skip to content

Tự làm Image Slideshow trong React, không cần SwipeJS


Khi cần một slideshow, phản xạ đầu tiên của nhiều người là mở npm lên và cài SwiperJS. Hoàn toàn hợp lý — Swiper mạnh, có sẵn mọi thứ. Nhưng nếu bạn chỉ cần một slideshow đơn giản, không có touch gesture, không có lazy load phức tạp, thì một vài chục dòng React là đủ.
Trong bài này, chúng ta sẽ tự xây từ con số 0 — bắt đầu từ bản đơn giản nhất, rồi lần lượt thêm nút điều hướng, numbered buttons, và cuối cùng là progress bar kiểu Stories cho ngầu.

Bước 1 — Slideshow cơ bản (chỉ CSS transition)

Ý tưởng cốt lõi rất đơn giản: render tất cả ảnh ra cùng lúc, nhưng chỉ một ảnh có opacity: 1, các ảnh còn lại opacity: 0. Mỗi 5 giây, chuyển class active sang ảnh tiếp theo.
// components/ImageSlideshow.tsx
'use client'

import { useState } from 'react'
import Image from 'next/image'
import burgerImg from '@/assets/burger.jpg'
import curryImg from '@/assets/curry.jpg'
import pizzaImg from '@/assets/pizza.jpg'
import classes from './ImageSlideshow.module.css'

const images = [
{ src: burgerImg, alt: 'A juicy burger' },
{ src: curryImg, alt: 'A spicy curry' },
{ src: pizzaImg, alt: 'A fresh pizza' },
]

export default function ImageSlideshow() {
const [currentIndex, setCurrentIndex] = useState(0)

return (
<div className={classes.slideshow}>
{images.map((image, index) => (
<Image
key={index}
src={image.src}
alt={image.alt}
className={index === currentIndex ? classes.active : ''}
/>
))}
</div>
)
}
/* ImageSlideshow.module.css */
.slideshow {
position: relative;
width: 100%;
height: 400px;
border-radius: 8px;
overflow: hidden;
}

.slideshow img {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute; /* xếp chồng tất cả ảnh lên nhau */
top: 0;
left: 0;
opacity: 0;
transform: scale(1.05);
transition: opacity 0.5s ease, transform 0.5s ease;
}

.slideshow .active {
opacity: 1;
transform: scale(1);
z-index: 1;
}
Hiện tại currentIndex không bao giờ thay đổi — slideshow đang đứng yên. Thêm useEffect để tự động chuyển ảnh:
import { useState, useEffect } from 'react'

// bên trong component:
useEffect(() => {
const interval = setInterval(() => {
setCurrentIndex(prev => (prev + 1) % images.length)
}, 5000)

return () => clearInterval(interval) // dọn dẹp khi unmount
}, [])
% images.length là trick để tự động quay vòng — khi index đến cuối mảng sẽ về lại 0.

Bước 2 — Thêm nút Prev / Next

Thêm hai hàm điều hướng và render hai nút:
export default function ImageSlideshow() {
const [currentIndex, setCurrentIndex] = useState(0)

useEffect(() => {
const interval = setInterval(() => {
setCurrentIndex(prev => (prev + 1) % images.length)
}, 5000)
return () => clearInterval(interval)
}, [])

function goToPrev() {
setCurrentIndex(prev => (prev - 1 + images.length) % images.length)
}

function goToNext() {
setCurrentIndex(prev => (prev + 1) % images.length)
}

return (
<div className={classes.slideshow}>
{images.map((image, index) => (
<Image
key={index}
src={image.src}
alt={image.alt}
className={index === currentIndex ? classes.active : ''}
/>
))}

<button className={classes.prev} onClick={goToPrev}></button>
<button className={classes.next} onClick={goToNext}></button>
</div>
)
}
.prev,
.next {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 2;
background: rgba(0, 0, 0, 0.4);
color: white;
border: none;
padding: 0.5rem 1rem;
font-size: 1.5rem;
cursor: pointer;
border-radius: 4px;
transition: background 0.2s;
}

.prev:hover,
.next:hover {
background: rgba(0, 0, 0, 0.7);
}

.prev { left: 0.75rem; }
.next { right: 0.75rem; }
Lưu ý công thức (prev - 1 + images.length) % images.length cho nút Prev — cộng thêm images.length trước khi mod để tránh kết quả âm khi prev = 0.

Bước 3 — Thêm Numbered Slide Buttons (Dots)

Thêm một row dots phía dưới, mỗi dot tương ứng một slide:
return (
<div className={classes.slideshow}>
{/* ... ảnh và nút prev/next ... */}

<div className={classes.dots}>
{images.map((_, index) => (
<button
key={index}
className={`${classes.dot} ${index === currentIndex ? classes.dotActive : ''}`}
onClick={() => setCurrentIndex(index)}
Want to print your doc?
This is not the way.
Try clicking the ··· in the right corner or using a keyboard shortcut (
CtrlP
) instead.