Skip to main content
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
ImageSlideshow.module.css
Hiện tại currentIndex không bao giờ thay đổi — slideshow đang đứng yên. Thêm useEffect để tự động chuyển ảnh:
% images.length là trick để tự động quay vòng — khi index đến cuối mảng sẽ về lại 0. Giả sử có 3 ảnh, images.length = 3, index hợp lệ là 0, 1, 2:
% (modulo) trả về phần dư của phép chia. 3 % 3 = 0 vì 3 chia 3 dư 0. 4 % 3 = 1 vì 4 chia 3 dư 1 — nên nó luôn giữ kết quả trong khoảng 0 đến length - 1. Tương tự cho nút Prev, thêm images.length trước để tránh số âm:
Nếu không cộng + 3, (0 - 1) % 3 = -1 — index âm, không dùng được.

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

Thêm hai hàm điều hướng và render hai nút:
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:
Khi click vào một dot, setCurrentIndex(index) đưa trực tiếp đến slide đó — không cần next/prev liên tục.

Bước 4 — Auto-play với Progress Bar kiểu Stories

Đây là phần thú vị nhất. Thay vì một thanh progress bar “đếm ngược” chung cho toàn bộ slideshow, chúng ta làm theo kiểu Instagram Stories: mỗi slide có một thanh riêng, thanh của slide đang hiển thị sẽ fill từ 0% đến 100% trong 5 giây. Trick ở đây là dùng CSS @keyframes animation kết hợp với key prop của React. Khi key thay đổi, React unmount và remount element — điều này buộc animation CSS restart từ đầu.
Animation fillProgress chạy trong 5s — khớp với interval của setInterval. Mỗi lần slide đổi, key={currentIndex} thay đổi và React remount <div className={progressFill}>, animation bắt đầu lại từ 0%. Các slide đã xem (index < currentIndex) hiển thị thanh trắng đầy tĩnh. Các slide chưa đến thì chỉ có track xám.

Lưu ý thực tế

Đừng để interval và animation lệch nhau. Nếu setInterval là 5000ms mà animation là 4s, thanh sẽ đầy trước khi slide chuyển — trông sẽ lạ. Luôn giữ hai giá trị này đồng bộ. Cách tốt hơn là define một constant:
Pause khi hover. Nếu muốn dừng slideshow khi người dùng hover vào, dùng onMouseEnter / onMouseLeave để clear và set lại interval. Cách gọn nhất là dùng useRef để giữ reference của interval thay vì useEffect thuần. Swipe trên mobile. Code trên không có touch gesture. Nếu cần, đây là lúc cân nhắc SwiperJS hoặc react-swipeable — đừng tự handle touchstart/touchmove bằng tay nếu không có nhu cầu học sâu về nó.

Tóm tắt

Từ một slideshow tự chạy đến Stories-style chỉ cần khoảng 80 dòng code và không có dependency nào ngoài React. Khi nào cần thêm touch, infinite loop mượt hơn, hay lazy load — lúc đó mới cần đến Swiper.
Last modified on June 11, 2026