javascriptresponsive-designcarouselgalleryinfinity

How to create an infinity carousel with touch in plain JS and CSS?


Here's an example of an infinity multi-item image carousel with touch using plain vanilla JS and CSS.

No dependencies, no frameworks.


Solution

  • 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:

    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;
        }
    }
    

    CodePen