Giới thiệu
Slideshow trích dẫn (quote carousel) là thành phần UI phổ biến để hiển thị lời nói nổi tiếng, đánh giá khách hàng, hoặc highlight nội dung. Bài viết hướng dẫn xây dựng slideshow tự động/chuyển slide thủ công với CSS + Vanilla JS.
Ví dụ hoàn chỉnh: Slideshow có điều khiển Prev/Next + Dots
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Quote Slideshow</title>
<style>
* { box-sizing: border-box; }
body { font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; text-align: center; padding: 2rem; background: #fafafa; }
h1 { margin-bottom: 1.5rem; color: #333; }
.slideContainer {
max-width: 700px; margin: 0 auto; position: relative;
background: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.08);
padding: 3rem 2rem; overflow: hidden;
}
.Slide { display: none; animation: fadeIn 0.4s ease; }
.Slide.active { display: block; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.Slide h1 { font-size: 1.8rem; line-height: 1.5; margin: 0 0 1.5rem; color: #222; font-style: italic; }
.Caption { color: #666; font-size: 1.1rem; font-weight: 500; }
/* Navigation Buttons */
.prevBtn, .nextBtn {
position: absolute; top: 50%; transform: translateY(-50%);
width: 48px; height: 48px; border-radius: 50%;
background: #fff; border: 1px solid #e0e0e0; box-shadow: 0 2px 8px rgba(0,0,0,0.1);
color: #500070; font-size: 1.5rem; font-weight: bold; cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: all 0.2s; z-index: 10;
}
.prevBtn:hover, .nextBtn:hover { background: #500070; color: #fff; border-color: #500070; }
.prevBtn { left: 16px; }
.nextBtn { right: 16px; }
/* Dots Navigation */
.dotsContainer { margin-top: 1.5rem; display: flex; justify-content: center; gap: 8px; }
.Navdot {
width: 12px; height: 12px; border-radius: 50%;
background: #ddd; cursor: pointer; transition: all 0.3s;
border: none; padding: 0;
}
.Navdot:hover { background: #500070; }
.Navdot.active { background: #500070; transform: scale(1.2); }
@media (max-width: 480px) {
.slideContainer { padding: 2rem 1rem; }
.Slide h1 { font-size: 1.4rem; }
.prevBtn, .nextBtn { width: 40px; height: 40px; font-size: 1.2rem; }
.prevBtn { left: 8px; } .nextBtn { right: 8px; }
}
</style>
</head>
<body>
<h1>Quote Slideshow</h1>
<div class="slideContainer">
<div class="Slide active">
<h1>"The way to get started is to quit talking and begin doing."</h1>
<div class="Caption">— Walt Disney</div>
</div>
<div class="Slide">
<h1>"If life were predictable it would cease to be life, and be without flavor."</h1>
<div class="Caption">— Eleanor Roosevelt</div>
</div>
<div class="Slide">
<h1>"Life is what happens when you're busy making other plans."</h1>
<div class="Caption">— John Lennon</div>
</div>
<button class="prevBtn" aria-label="Previous quote">❮</button>
<button class="nextBtn" aria-label="Next quote">❯</button>
</div>
<div class="dotsContainer" aria-label="Quote navigation">
<button class="Navdot active" data-slide="0" aria-label="Quote 1"></button>
<button class="Navdot" data-slide="1" aria-label="Quote 2"></button>
<button class="Navdot" data-slide="2" aria-label="Quote 3"></button>
</div>
<script>
(function() {
const slides = document.querySelectorAll('.Slide');
const dots = document.querySelectorAll('.Navdot');
const prevBtn = document.querySelector('.prevBtn');
const nextBtn = document.querySelector('.nextBtn');
let currentIndex = 0;
let autoPlayInterval;
function showSlide(index) {
// Wrap around
if (index >= slides.length) index = 0;
if (index < 0) index = slides.length - 1;
slides.forEach(s => s.classList.remove('active'));
dots.forEach(d => d.classList.remove('active'));
slides[index].classList.add('active');
dots[index].classList.add('active');
currentIndex = index;
}
function nextSlide() { showSlide(currentIndex + 1); }
function prevSlide() { showSlide(currentIndex - 1); }
// Event listeners
nextBtn.addEventListener('click', nextSlide);
prevBtn.addEventListener('click', prevSlide);
dots.forEach((dot, i) => {
dot.addEventListener('click', () => showSlide(i));
});
// Auto-play (optional)
function startAutoPlay() {
autoPlayInterval = setInterval(nextSlide, 5000);
}
function stopAutoPlay() { clearInterval(autoPlayInterval); }
// Pause on hover
const container = document.querySelector('.slideContainer');
container.addEventListener('mouseenter', stopAutoPlay);
container.addEventListener('mouseleave', startAutoPlay);
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') prevSlide();
if (e.key === 'ArrowRight') nextSlide();
});
startAutoPlay();
})();
</script>
</body>
</html>
Phân tích kỹ thuật
CSS
- .Slide:
display: nonemặc định, chỉ hiển thị khi có class.active. - Animation:
@keyframes fadeIntạo hiệu ứng mượt mà khi chuyển slide. - Position absolute: Nút prev/next đặt tuyệt đối so với
.slideContainer(position: relative). - Responsive: Giảm padding, font-size, nút điều khiển trên mobile.
JavaScript (Vanilla, ES6+)
- IIFE: Bọc code tránh ô nhiễm global scope.
- State management: Biến
currentIndextheo dõi slide hiện tại. - Modular functions:
showSlide(),nextSlide(),prevSlide()tách biệt logic. - Auto-play với pause on hover: Tăng UX, dùng
setInterval/clearInterval. - Keyboard support: Phím mũi tên trái/phải để điều hướng (Accessibility).
- ARIA attributes:
aria-labelcho nút, dots - hỗ trợ screen reader.
Mở rộng: Infinite Loop Carousel (CSS-only)
Để slideshow lặp vô tận mượt mà chỉ dùng CSS (animation), cần duplicate các slide đầu/cuối và dùng @keyframes translateX. Xem bài 1003063647 để biết chi tiết.