Here's an example of an infinity multi-item image carousel with touch using plain vanilla JS and CSS.
No dependencies, no frameworks.
In a recent project, I implemented an image carousel with a focus on efficiency and responsiveness, and I'd like to share the rationale behind my design and coding decisions. Let's break it down:
HTML Structure
The carousel is structured with a <div>
containing all the carousel images. Notably, I've used "eager" loading for the first and last three images. This is because these images are likely to be in or near the viewport when the carousel is initially loaded or when users navigate to the end. Eager loading ensures these images are loaded as soon as possible, enhancing user experience.
CSS Design
I've used CSS custom properties (variables) for easier maintenance and better scalability. The responsive design is achieved using media queries. Below 576px width, the carousel displays one image at a time (100% width), and above that, it shows three images at a time (33.33% width). The display: none
for navigation buttons on smaller screens enhances mobile user experience by providing more space.
JavaScript Functionality
The JavaScript class Carousel
handles all carousel functionalities. It initializes with the current index set to the first real image, considering the duplicated (fake) images for the loop effect. The slide
function updates the index and uses CSS transitions for smooth movement. Special attention is given to touch navigation, a must-have for modern web interfaces, allowing users to navigate the carousel with swipes.
Key Considerations
This implementation aligns with modern web development standards focusing on performance, responsiveness, and user experience.
<div class="image-carousel">
<div class="carousel-viewport">
<!--/ You need to fake last three items (use eager loading!) /-->
<div class="carousel-image"><img src="https://i.ibb.co/Gv9jq4z/sci-fi-scene-ossenbrueck-4.png" class="img-fluid" alt="..." loading="eager"></div>
<div class="carousel-image"><img src="https://i.ibb.co/dQmdmM3/sci-fi-scene-ossenbrueck-5.png" class="img-fluid" alt="..." loading="eager"></div>
<div class="carousel-image"><img src="https://i.ibb.co/LprBxDc/sci-fi-scene-ossenbrueck-6.png" class="img-fluid" alt="..." loading="eager"></div>
<!--/ All images/-->
<div class="carousel-image"><img src="https://i.ibb.co/Kr8N8D4/sci-fi-scene-ossenbrueck-1.png" class="img-fluid" alt="..." loading="lazy"></div>
<div class="carousel-image"><img src="https://i.ibb.co/pXvLw1D/sci-fi-scene-ossenbrueck-2.png" class="img-fluid" alt="..." loading="lazy"></div>
<div class="carousel-image"><img src="https://i.ibb.co/6Dg7bY1/sci-fi-scene-ossenbrueck-3.png" class="img-fluid" alt="..." loading="lazy"></div>
<div class="carousel-image"><img src="https://i.ibb.co/Gv9jq4z/sci-fi-scene-ossenbrueck-4.png" class="img-fluid" alt="..." loading="lazy"></div>
<div class="carousel-image"><img src="https://i.ibb.co/dQmdmM3/sci-fi-scene-ossenbrueck-5.png" class="img-fluid" alt="..." loading="lazy"></div>
<div class="carousel-image"><img src="https://i.ibb.co/LprBxDc/sci-fi-scene-ossenbrueck-6.png" class="img-fluid" alt="..." loading="lazy"></div>
<!--/ You need to fake first three items (use eager loading!) /-->
<div class="carousel-image"><img src="https://i.ibb.co/Kr8N8D4/sci-fi-scene-ossenbrueck-1.png" class="img-fluid" alt="..." loading="eager"></div>
<div class="carousel-image"><img src="https://i.ibb.co/pXvLw1D/sci-fi-scene-ossenbrueck-2.png" class="img-fluid" alt="..." loading="eager"></div>
<div class="carousel-image"><img src="https://i.ibb.co/6Dg7bY1/sci-fi-scene-ossenbrueck-3.png" class="img-fluid" alt="..." loading="eager"></div>
</div>
<button class="carousel-control prev">Prev</button>
<button class="carousel-control next">Next</button>
</div>
document.addEventListener('DOMContentLoaded', () => {
class Carousel {
constructor(viewportSelector, prevSelector, nextSelector) {
this.viewport = document.querySelector(viewportSelector);
this.prevButton = document.querySelector(prevSelector);
this.nextButton = document.querySelector(nextSelector);
this.currentIndex = 3;
this.realImageCount = this.viewport.children.length / 2;
this.setupNavigation();
this.setupTouchNavigation();
this.renderViewport();
}
getImageWidth() {
return window.innerWidth <= 600 ? 100 : 33.33;
}
resetPositionFirst() {
this.viewport.classList.add('transition-disabled');
this.currentIndex = 3;
this.renderViewport();
this.viewport.offsetHeight; // Trigger reflow
this.viewport.classList.remove('transition-disabled');
this.slide(1);
}
resetPositionToEnd() {
this.viewport.classList.add('transition-disabled');
this.currentIndex = this.viewport.children.length - 6;
this.renderViewport();
this.viewport.offsetHeight; // Trigger reflow
this.viewport.classList.remove('transition-disabled');
this.slide(-1);
}
slide(direction) {
this.currentIndex += direction;
if (this.currentIndex > this.realImageCount + 4) {
this.resetPositionFirst();
} else if (this.currentIndex < 0) {
this.resetPositionToEnd();
} else {
this.renderViewport();
}
}
renderViewport() {
this.viewport.style.transform = `translateX(-${this.currentIndex * this.getImageWidth()}%)`;
}
setupNavigation() {
this.prevButton.addEventListener('click', () => this.slide(-1));
this.nextButton.addEventListener('click', () => this.slide(1));
}
setupTouchNavigation() {
let touchStartX = 0;
let touchEndX = 0;
this.viewport.addEventListener('touchstart', e => touchStartX = e.touches[0].clientX, {passive: true});
this.viewport.addEventListener('touchmove', e => touchEndX = e.touches[0].clientX, {passive: true});
this.viewport.addEventListener('touchend', () => {
if (touchStartX - touchEndX > 50) this.slide(1);
if (touchEndX - touchStartX > 50) this.slide(-1);
});
}
}
new Carousel('.carousel-viewport', '.prev', '.next');
});
:root {
--carousel-control-bg: #fff;
--carousel-img-width: 33.33%;
--carousel-mobile-img-width: 100%;
}
.img-fluid {
width: 100%;
height: auto;
}
.image-carousel {
position: relative;
width: 100%;
overflow: hidden;
}
.carousel-viewport {
display: flex;
transition: transform 0.4s ease;
}
.carousel-viewport .carousel-image {
flex: 0 0 var(--carousel-mobile-img-width);
max-width: var(--carousel-mobile-img-width);
}
.carousel-control {
display: none;
position: absolute;
top: 50%;
background-color: #fff;
border: none;
cursor: pointer;
padding: 10px;
transform: translateY(-50%);
z-index: 2;
}
.carousel-control.prev {
left: 10px;
}
.carousel-control.next {
right: 10px;
}
.transition-disabled {
transition: none !important;
}
@media (min-width: 576px) {
.carousel-viewport .carousel-image {
flex: 0 0 var(--carousel-img-width);
max-width: var(--carousel-img-width);
}
.carousel-control {
display: block;
}
}