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 ();
}
}
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
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
CSS selector for the carousel track container
CSS selector for individual carousel cards
CSS selector for the previous button
CSS selector for the next button
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-track {
display : flex ;
gap : 1.5 rem ;
transition : transform 0.4 s cubic-bezier ( 0.25 , 0.46 , 0.45 , 0.94 );
}
.carousel-track.dragging {
transition : none ;
cursor : grabbing ;
}
Card Hover Effects
.carousel-professional-work-card:hover ,
.carousel-personal-project-card:hover {
border-color : var ( --color-border-hover );
transform : translateY ( -5 px );
}
Transform3D for GPU Acceleration
RequestAnimationFrame for Smooth Dragging
Drag updates are batched using requestAnimationFrame: this . animationID = requestAnimationFrame (() => {
this . track . style . transition = 'none' ;
this . track . style . transform = `translate3d( ${ this . currentTranslate } px, 0, 0)` ;
});
Debounced Resize Handling
Window resize events are debounced to prevent excessive recalculations: window . addEventListener ( 'resize' , () => {
clearTimeout ( this . resizeTimer );
this . resizeTimer = setTimeout (() => {
this . updateDimensions ();
this . updatePosition ( false );
}, 100 );
});
ResizeObserver for Container Changes
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