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
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:
+ 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:(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: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.
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ếusetInterval 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:
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
| Version | Tính năng thêm | Kỹ thuật chính |
|---|---|---|
| Cơ bản | Tự chạy | setInterval + CSS opacity |
| + Prev/Next | Điều hướng thủ công | Modulo % để wrap index |
| + Dots | Click thẳng vào slide | Map index → button |
| + Progress bar | Feedback trực quan Stories-style | CSS @keyframes + React key để restart |