This carousel uses CSS scroll snap to create smooth, performant slide transitions that do not cause layout shifts.
This carousel can be navigated in a variety of ways: in addition to navigation controls, it supports keyboard navigation and swiping. To maximize usability and readability, the carousel stops auto-transitioning once the user mouseovers within the carousel area.
<div id="carousel">
<div id="slide-container">
<div class="slide" data-slideIndex="0">
<div class="slide-banner">Tour the Empire State Building! <a href="">Buy tickets now.</a></div>
<img width="1200" height="600" src="./newyork.jpg">
</div>
<div class="slide" data-slideIndex="1">
<div class="slide-banner">Ride the Shinkansen! <a href="">Buy tickets now.</a></div>
<img width="1200" height="600" src="./tokyo.jpg">
</div>
<div class="slide" data-slideIndex="2">
<div class="slide-banner">Discover relaxation! <a href="">Buy tickets now.</a></div>
<img width="1200" height="600" src="./beach.jpg">
</div>
<div class="slide" data-slideIndex="3">
<div class="slide-banner">See penguins! <a href="">Buy tickets now.</a></div>
<img width="1200" height="600" src="./penguins.jpg">
</div>
<div class="slide" data-slideIndex="4">
<div class="slide-banner">Take a ride on the wheel! <a href="">Buy tickets now.</a></div>
<img width="1200" height="600" src="./wheel.jpg">
</div>
</div>
<div id="back-button" class="arrow back">←</div>
<div id="forward-button" class="arrow forward">→</div>
<div class="slide-indicators">
<div class="slide-indicator active"></div>
<div class="slide-indicator"></div>
<div class="slide-indicator"></div>
<div class="slide-indicator"></div>
<div class="slide-indicator"></div>
</div>
</div>
#carousel {
max-width: 1200px;
display: flex;
flex-direction: column;
margin: 0 auto;
position: relative;
}
.slide-indicators {
display: flex;
justify-content: center;
}
.slide-indicator {
height: 44px;
width: 50px;
display: flex;
justify-items: center;
cursor: pointer;
}
.slide-indicator:after {
content: "";
background-color: #878787;
height: 10px;
margin-top: 10px;
width: 40px;
}
.slide-indicator.active:after,
.slide-indicator:hover:after {
background-color: #000;
}
.slide-banner {
background-color: #000;
color: #fff;
position: absolute;
left: 0;
bottom: 20px;
padding: 15px;
font-size: 2.5vw;
}
.slide-banner a {
color: #fff;
}
#slide-container {
scroll-snap-type: x mandatory;
overflow-x: scroll;
overflow-y: hidden;
display: flex;
align-items: center;
height: 100%;
gap: 10px;
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
}
.slide {
scroll-snap-align: center;
position: relative;
min-width: 100%;
padding-top: 50%;
}
.slide img {
height: 100%;
width: auto;
position: absolute;
top: 0;
left: 0;
}
.arrow {
color: #fff;
height: 20px;
width: 20px;
background-color: #000;
position: absolute;
padding: 10px;
opacity: .3;
cursor: pointer;
}
.arrow.back {
left: 10px;
top: 10px;
}
.arrow.forward {
right: 10px;
top: 10px;
}
.arrow:hover {
opacity: 1;
}
function autoplayCarousel() {
const carouselEl = document.getElementById("carousel");
const slideContainerEl = carouselEl.querySelector("#slide-container");
const slideEl = carouselEl.querySelector(".slide");
let slideWidth = slideEl.offsetWidth;
// Add click handlers
document.querySelector("#back-button")
.addEventListener("click", () => navigate("backward"));
document.querySelector("#forward-button")
.addEventListener("click", () => navigate("forward"));
document.querySelectorAll(".slide-indicator")
.forEach((dot, index) => {
dot.addEventListener("click", () => navigate(index));
dot.addEventListener("mouseenter", () => clearInterval(autoplay));
});
// Add keyboard handlers
document.addEventListener('keydown', (e) => {
if (e.code === 'ArrowLeft') {
clearInterval(autoplay);
navigate("backward");
} else if (e.code === 'ArrowRight') {
clearInterval(autoplay);
navigate("forward");
}
});
// Add resize handler
window.addEventListener('resize', () => {
slideWidth = slideEl.offsetWidth;
});
// Autoplay
const autoplay = setInterval(() => navigate("forward"), 3000);
slideContainerEl.addEventListener("mouseenter", () => clearInterval(autoplay));
// Slide transition
const getNewScrollPosition = (arg) => {
const gap = 10;
const maxScrollLeft = slideContainerEl.scrollWidth - slideWidth;
if (arg === "forward") {
const x = slideContainerEl.scrollLeft + slideWidth + gap;
return x <= maxScrollLeft ? x : 0;
} else if (arg === "backward") {
const x = slideContainerEl.scrollLeft - slideWidth - gap;
return x >= 0 ? x : maxScrollLeft;
} else if (typeof arg === "number") {
const x = arg * (slideWidth + gap);
return x;
}
}
const navigate = (arg) => {
slideContainerEl.scrollLeft = getNewScrollPosition(arg);
}
// Slide indicators
const slideObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const slideIndex = entry.target.dataset.slideindex;
carouselEl.querySelector('.slide-indicator.active').classList.remove('active');
carouselEl.querySelectorAll('.slide-indicator')[slideIndex].classList.add('active');
}
});
}, { root: slideContainerEl, threshold: .1 });
document.querySelectorAll('.slide').forEach((slide) => {
slideObserver.observe(slide);
});
}
autoplayCarousel();