Skip to main content

Overview

The Dev Showcase carousel is a custom-built infinite scrolling component that provides smooth navigation through project cards. It features touch and mouse drag support, automatic cloning for seamless infinite scrolling, and responsive behavior.

Key Features

Infinite Scrolling

Seamless looping using cloned elements for continuous navigation

Touch & Mouse Support

Full touch gestures and mouse drag functionality

Responsive Design

Adapts to different screen sizes with dynamic slide widths

Smooth Transitions

Cubic bezier easing for professional animations

Architecture

InfiniteCarousel Class

The carousel is implemented as a JavaScript class with the following structure:
carousel.js (lines 19-46)
class InfiniteCarousel {
    constructor({ trackSelector, cardSelector, prevBtnSelector, nextBtnSelector, paginationContainer }) {
        this.track = document.querySelector(trackSelector);
        this.originalCards = document.querySelectorAll(cardSelector);
        this.prevBtn = document.querySelector(prevBtnSelector);
        this.nextBtn = document.querySelector(nextBtnSelector);
        this.currentSpan = document.querySelector(`${paginationContainer} .current`);
        this.totalSpan = document.querySelector(`${paginationContainer} .total`);

        if (!this.track || this.originalCards.length === 0) return;

        this.totalOriginal = this.originalCards.length;
        this.clonesCount = Math.max(this.totalOriginal, Math.ceil(this.getVisibleSlides()) + 3);
        this.currentIndex = this.clonesCount;
        this.isTransitioning = false;
        this.slideWidth = 0;

        this.isDragging = false;
        this.startPos = 0;
        this.currentTranslate = 0;
        this.prevTranslate = 0;
        this.animationID = 0;
        this.hasMoved = false;

        this.resizeTimer = null;

        this.init();
    }
}

Infinite Scrolling Implementation

The carousel creates clones of the original cards to achieve seamless infinite scrolling:
carousel.js (lines 82-98)
createCl ones() {
    // Clone cards at the end
    for (let i = 0; i < this.clonesCount; i++) {
        const clone = this.originalCards[i % this.totalOriginal].cloneNode(true);
        clone.classList.add('clone');
        this.track.appendChild(clone);
    }

    // Clone cards at the beginning (in reverse)
    for (let i = 0; i < this.clonesCount; i++) {
        const index = (this.totalOriginal - 1 - (i % this.totalOriginal));
        const safeIndex = ((index % this.totalOriginal) + this.totalOriginal) % this.totalOriginal;
        const clone = this.originalCards[safeIndex].cloneNode(true);
        clone.classList.add('clone');
        this.track.insertBefore(clone, this.track.firstChild);
    }

    this.allSlides = this.track.children;
}

Position Reset Logic

When the user reaches a clone, the carousel instantly resets to the corresponding original card:
carousel.js (lines 129-139)
handleTransitionEnd() {
    this.isTransitioning = false;

    if (this.currentIndex >= this.totalOriginal + this.clonesCount) {
        this.currentIndex -= this.totalOriginal;
        this.updatePosition(false);
    } else if (this.currentIndex < this.clonesCount) {
        this.currentIndex += this.totalOriginal;
        this.updatePosition(false);
    }
}

Touch and Mouse Support

The carousel supports both touch gestures and mouse dragging:
carousel.js (lines 147-158)
addTouchEvents() {
    this.track.addEventListener('touchstart', this.touchStart.bind(this), { passive: true });
    this.track.addEventListener('touchmove', this.touchMove.bind(this), { passive: false });
    this.track.addEventListener('touchend', this.touchEnd.bind(this));

    this.track.addEventListener('mousedown', this.touchStart.bind(this));
    this.track.addEventListener('mousemove', this.touchMove.bind(this));
    this.track.addEventListener('mouseup', this.touchEnd.bind(this));
    this.track.addEventListener('mouseleave', () => { 
        if (this.isDragging) this.touchEnd(); 
    });

    this.track.oncontextmenu = (e) => { e.preventDefault(); e.stopPropagation(); return false; };
}

Drag Logic

The drag functionality calculates slide changes based on movement distance:
carousel.js (lines 191-215)
touchEnd() {
    if (!this.isDragging) return;
    this.isDragging = false;
    this.track.classList.remove('dragging');

    if (!this.hasMoved) {
        this.updatePosition(false);
        return;
    }

    const movedBy = this.currentTranslate - this.prevTranslate;
    const rawSlidesChanged = -movedBy / this.slideWidth;
    let slidesJump = Math.round(rawSlidesChanged);

    // Minimum drag threshold
    if (slidesJump === 0 && Math.abs(movedBy) > 50) {
        slidesJump = movedBy < 0 ? 1 : -1;
    }

    this.currentIndex += slidesJump;
    this.isTransitioning = true;
    this.track.style.transition = 'transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94)';

    this.updatePosition(true);
    this.updatePagination();
}

Configuration

Initialization Example

carousel.js (lines 1-17)
document.addEventListener('DOMContentLoaded', () => {
    new InfiniteCarousel({
        trackSelector: '.carousel-personal-project-track',
        cardSelector: '.carousel-personal-project-card',
        prevBtnSelector: '.carousel-personal-project-arrow.left',
        nextBtnSelector: '.carousel-personal-project-arrow.right',
        paginationContainer: '.carousel-personal-project-pagination'
    });

    new InfiniteCarousel({
        trackSelector: '.carousel-professional-work-track',
        cardSelector: '.carousel-professional-work-card',
        prevBtnSelector: '.carousel-professional-work-arrow.left',
        nextBtnSelector: '.carousel-professional-work-arrow.right',
        paginationContainer: '.carousel-professional-work-pagination'
    });
});

Configuration Options

trackSelector
string
required
CSS selector for the carousel track container
cardSelector
string
required
CSS selector for individual carousel cards
prevBtnSelector
string
required
CSS selector for the previous button
nextBtnSelector
string
required
CSS selector for the next button
paginationContainer
string
required
CSS selector for the pagination display container

Responsive Behavior

The carousel adapts to different screen sizes:
carousel.js (lines 48-50)
getVisibleSlides() {
    return window.innerWidth > 1200 ? 2.1 : 1.1;
}
  • Desktop (>1200px): Shows 2.1 slides
  • Mobile (≤1200px): Shows 1.1 slides
The partial slide (.1) provides a visual hint that more content is available.

Transitions and Animations

CSS Transitions

carousel.css
.carousel-track {
    display: flex;
    gap: 1.5rem;
    transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.carousel-track.dragging {
    transition: none;
    cursor: grabbing;
}

Card Hover Effects

carousel.css
.carousel-professional-work-card:hover,
.carousel-personal-project-card:hover {
    border-color: var(--color-border-hover);
    transform: translateY(-5px);
}

Performance Optimizations

The carousel uses translate3d() instead of translateX() to leverage GPU acceleration:
this.track.style.transform = `translate3d(${pos}px, 0, 0)`;
Drag updates are batched using requestAnimationFrame:
this.animationID = requestAnimationFrame(() => {
    this.track.style.transition = 'none';
    this.track.style.transform = `translate3d(${this.currentTranslate}px, 0, 0)`;
});
Window resize events are debounced to prevent excessive recalculations:
window.addEventListener('resize', () => {
    clearTimeout(this.resizeTimer);
    this.resizeTimer = setTimeout(() => {
        this.updateDimensions();
        this.updatePosition(false);
    }, 100);
});
A ResizeObserver monitors the track for dimension changes:
const resizeObserver = new ResizeObserver(() => {
    clearTimeout(this.resizeTimer);
    this.resizeTimer = setTimeout(() => {
        this.updateDimensions();
        this.updatePosition(false);
    }, 100);
});
resizeObserver.observe(this.track);

Click vs Drag Detection

The carousel distinguishes between clicks and drags to handle card selection:
carousel.js (lines 217-248)
setupCardClickListeners() {
    this.track.addEventListener('click', (e) => {
        if (this.hasMoved) {
            e.preventDefault();
            e.stopPropagation();
            return;
        }

        const card = e.target.closest('.carousel-card');
        if (!card || card.classList.contains('clone')) return;

        const projectKey = card.dataset.project;
        if (!projectKey) return;

        const infoCard = document.getElementById(`${projectKey}-info`);
        if (!infoCard) return;

        document.querySelectorAll('.project-info-card').forEach(c => 
            c.classList.remove('active')
        );
        infoCard.classList.add('active');

        const parentSection = document.querySelector('.content-section[data-content="projects"]');
        const infoContainer = document.querySelector('.carousel-information-container');

        if (parentSection && infoContainer) {
            setTimeout(() => {
                parentSection.scrollTo({
                    top: infoContainer.offsetTop - 20,
                    behavior: 'smooth'
                });
            }, 450);
        }
    }, { capture: true });
}

Usage Example

To use the carousel in your HTML:
<div class="carousel-personal-project-container">
    <div class="carousel-personal-project-track">
        <div class="carousel-personal-project-card carousel-card" data-project="project1">
            <img src="project1.jpg" alt="Project 1">
            <div class="personal-project-content">
                <h3>Project Name</h3>
                <p>Project description</p>
            </div>
        </div>
        <!-- More cards... -->
    </div>
    
    <div class="carousel-arrow carousel-personal-project-arrow left">
        <svg><!-- Arrow icon --></svg>
    </div>
    <div class="carousel-arrow carousel-personal-project-arrow right">
        <svg><!-- Arrow icon --></svg>
    </div>
    
    <div class="carousel-personal-project-pagination">
        <span class="current">1</span> / <span class="total">5</span>
    </div>
</div>
The carousel automatically initializes on DOMContentLoaded and requires no manual initialization beyond including the script.

Browser Compatibility

  • Modern browsers: Full support (Chrome, Firefox, Safari, Edge)
  • Touch events: iOS Safari, Android Chrome
  • ResizeObserver: All modern browsers (polyfill available for older browsers)
  • CSS transforms: All modern browsers with GPU acceleration

Build docs developers (and LLMs) love