I've been working on this draggable/scrollable infinite carousel for a few hours but I ran into an issue. When scrolling 'too fast', carousel.scrollLeft()
seems to not be able to keep up and leaves gaps in between the values as seen below in the screenshot.
For my infinite caroussel to work I need carousel.scrollLeft()
to be equal to my trigger value, so it can 'teleport' back to the start/end.
I can't use if (carousel.scrollleft() <= scrollTriggerStart)
since that raises a whole bunch of other issues.
const carousel = $('.carousel');
const carouselCards = carousel.children('.card');
let cardAmount = 3; // The amount of cards visible within the carousel
carouselCards.slice(-cardAmount).get().reverse().forEach((e) => {
let card = $(e);
carousel.prepend(card.prop('outerHTML')); // Puts the last three cards in front of the first card in reverse order
})
carouselCards.slice(0, cardAmount).get().forEach((e) => {
let card = $(e);
carousel.append(card.prop('outerHTML')); // Puts the first three cards after the last card
})
let scrollTriggerStart = Math.round(carouselCards.outerWidth() + carouselCards.outerWidth() / 2 - carousel.outerWidth() / 2); // The exact scrollLeft value where the second card is in the center of the carousel
let scrollTriggerEnd = Math.round(carousel[0].scrollWidth - carouselCards.outerWidth() - carouselCards.outerWidth() / 2 - carousel.outerWidth() / 2); // The exact scrollLeft value where the second to last card is in the center of the carousel
let seamlessPosStart = carouselCards.length * carouselCards.outerWidth() - carouselCards.outerWidth() / 2 - carousel.outerWidth() / 2; // The exact scrollLeft value of the corresponding card of the second to last card
let seamlessPosEnd = carousel[0].scrollWidth - carouselCards.length * carouselCards.outerWidth() + carouselCards.outerWidth() / 2 - carousel.outerWidth() / 2; // The exact scrollLeft value of the corresponding card of the second card
infiniteScroll = () => {
if (carousel.scrollLeft() <= scrollTriggerStart) {
carousel.addClass('no-transition');
carousel.scrollLeft(seamlessPosEnd);
carousel.removeClass('no-transition');
} else if (carousel.scrollLeft() >= scrollTriggerEnd) {
carousel.addClass('no-transition');
carousel.scrollLeft(seamlessPosStart);
carousel.removeClass('no-transition');
}
}
carousel.on('scroll', infiniteScroll);
*,
::before,
::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
* {
font: inherit;
}
html,
body {
min-height: 100vh;
}
ul:has([class]) {
list-style: none;
}
body {
font-family: sans-serif;
display: flex;
align-items: center;
justify-content: center;
}
.wrapper {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 5em;
width: 100%;
padding-inline: 10em;
}
.carousel {
display: grid;
grid-auto-flow: column;
grid-auto-columns: calc(100% / 2);
overflow-x: auto;
scroll-snap-type: x mandatory;
border-radius: .5em;
scroll-behavior: smooth;
scrollbar-width: none;
width: 100%;
}
.carousel::-webkit-scrollbar {
display: none;
}
.carousel .card {
aspect-ratio: 1;
scroll-snap-align: center;
background: rgba(255, 0, 0, 0.25);
border-radius: .5rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
scale: 0.95;
border: 2px solid transparent;
transition: 250ms ease;
}
.card.selected {
border: 2px solid red;
scale: 1;
}
.no-transition {
scroll-behavior: auto;
}
.no-transition .card {
scroll-snap-align: unset;
}
/* .slider-interactive {
display: flex;
gap: 4em;
}
.slider-interactive > * {
background: rgba(255,0,0,0.25);
aspect-ratio: 1;
border-radius: 50%;
cursor: pointer;
transition: 250ms ease;
width: 1.5rem;
}
.slider-interactive > *.selected {
background: red;
} */
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css">
<title>Infinite carousel</title>
</head>
<body>
<div class="wrapper">
<ul class="carousel">
<li class="card">
<h2>Card 1</h2>
</li>
<li class="card">
<h2>Card 2</h2>
</li>
<li class="card">
<h2>Card 3</h2>
</li>
<li class="card">
<h2>Card 4</h2>
</li>
<li class="card">
<h2>Card 5</h2>
</li>
</ul>
</div>
<script src=".7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="main.js"></script>
</body>
</html>
I've been working on this draggable/scrollable infinite carousel for a few hours but I ran into an issue. When scrolling 'too fast', carousel.scrollLeft()
seems to not be able to keep up and leaves gaps in between the values as seen below in the screenshot.
For my infinite caroussel to work I need carousel.scrollLeft()
to be equal to my trigger value, so it can 'teleport' back to the start/end.
I can't use if (carousel.scrollleft() <= scrollTriggerStart)
since that raises a whole bunch of other issues.
const carousel = $('.carousel');
const carouselCards = carousel.children('.card');
let cardAmount = 3; // The amount of cards visible within the carousel
carouselCards.slice(-cardAmount).get().reverse().forEach((e) => {
let card = $(e);
carousel.prepend(card.prop('outerHTML')); // Puts the last three cards in front of the first card in reverse order
})
carouselCards.slice(0, cardAmount).get().forEach((e) => {
let card = $(e);
carousel.append(card.prop('outerHTML')); // Puts the first three cards after the last card
})
let scrollTriggerStart = Math.round(carouselCards.outerWidth() + carouselCards.outerWidth() / 2 - carousel.outerWidth() / 2); // The exact scrollLeft value where the second card is in the center of the carousel
let scrollTriggerEnd = Math.round(carousel[0].scrollWidth - carouselCards.outerWidth() - carouselCards.outerWidth() / 2 - carousel.outerWidth() / 2); // The exact scrollLeft value where the second to last card is in the center of the carousel
let seamlessPosStart = carouselCards.length * carouselCards.outerWidth() - carouselCards.outerWidth() / 2 - carousel.outerWidth() / 2; // The exact scrollLeft value of the corresponding card of the second to last card
let seamlessPosEnd = carousel[0].scrollWidth - carouselCards.length * carouselCards.outerWidth() + carouselCards.outerWidth() / 2 - carousel.outerWidth() / 2; // The exact scrollLeft value of the corresponding card of the second card
infiniteScroll = () => {
if (carousel.scrollLeft() <= scrollTriggerStart) {
carousel.addClass('no-transition');
carousel.scrollLeft(seamlessPosEnd);
carousel.removeClass('no-transition');
} else if (carousel.scrollLeft() >= scrollTriggerEnd) {
carousel.addClass('no-transition');
carousel.scrollLeft(seamlessPosStart);
carousel.removeClass('no-transition');
}
}
carousel.on('scroll', infiniteScroll);
*,
::before,
::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
* {
font: inherit;
}
html,
body {
min-height: 100vh;
}
ul:has([class]) {
list-style: none;
}
body {
font-family: sans-serif;
display: flex;
align-items: center;
justify-content: center;
}
.wrapper {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 5em;
width: 100%;
padding-inline: 10em;
}
.carousel {
display: grid;
grid-auto-flow: column;
grid-auto-columns: calc(100% / 2);
overflow-x: auto;
scroll-snap-type: x mandatory;
border-radius: .5em;
scroll-behavior: smooth;
scrollbar-width: none;
width: 100%;
}
.carousel::-webkit-scrollbar {
display: none;
}
.carousel .card {
aspect-ratio: 1;
scroll-snap-align: center;
background: rgba(255, 0, 0, 0.25);
border-radius: .5rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
scale: 0.95;
border: 2px solid transparent;
transition: 250ms ease;
}
.card.selected {
border: 2px solid red;
scale: 1;
}
.no-transition {
scroll-behavior: auto;
}
.no-transition .card {
scroll-snap-align: unset;
}
/* .slider-interactive {
display: flex;
gap: 4em;
}
.slider-interactive > * {
background: rgba(255,0,0,0.25);
aspect-ratio: 1;
border-radius: 50%;
cursor: pointer;
transition: 250ms ease;
width: 1.5rem;
}
.slider-interactive > *.selected {
background: red;
} */
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css">
<title>Infinite carousel</title>
</head>
<body>
<div class="wrapper">
<ul class="carousel">
<li class="card">
<h2>Card 1</h2>
</li>
<li class="card">
<h2>Card 2</h2>
</li>
<li class="card">
<h2>Card 3</h2>
</li>
<li class="card">
<h2>Card 4</h2>
</li>
<li class="card">
<h2>Card 5</h2>
</li>
</ul>
</div>
<script src="https://cdnjs.cloudflare/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="main.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Infinite Carousel</title>
<style>
*,
::before,
::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: #f0f0f0;
}
.wrapper {
width: 100%;
max-width: 800px;
overflow: hidden;
position: relative;
}
.carousel {
display: flex;
width: max-content;
transition: transform 0.5s ease;
}
.carousel .card {
flex: 0 0 auto;
width: 200px;
height: 200px;
margin: 0 10px;
background: rgba(255, 0, 0, 0.25);
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
scale: 0.95;
border: 2px solid transparent;
transition: 250ms ease;
}
.carousel .card.selected {
border: 2px solid red;
scale: 1;
}
.controls {
position: absolute;
top: 50%;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
transform: translateY(-50%);
pointer-events: none;
}
.controls button {
pointer-events: all;
background: rgba(0, 0, 0, 0.5);
border: none;
color: white;
font-size: 1.5rem;
padding: 10px;
cursor: pointer;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.controls button:hover {
background: rgba(0, 0, 0, 0.8);
}
</style>
</head>
<body>
<div class="wrapper">
<div class="carousel">
<div class="card">
<h2>Card 1</h2>
</div>
<div class="card">
<h2>Card 2</h2>
</div>
<div class="card">
<h2>Card 3</h2>
</div>
<div class="card">
<h2>Card 4</h2>
</div>
<div class="card">
<h2>Card 5</h2>
</div>
<!-- Duplicate cards for seamless looping -->
<div class="card">
<h2>Card 1</h2>
</div>
<div class="card">
<h2>Card 2</h2>
</div>
<div class="card">
<h2>Card 3</h2>
</div>
<div class="card">
<h2>Card 4</h2>
</div>
<div class="card">
<h2>Card 5</h2>
</div>
</div>
<div class="controls">
<button id="prevBtn">❮</button>
<button id="nextBtn">❯</button>
</div>
</div>
<script>
const carousel = document.querySelector('.carousel');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const cardWidth = document.querySelector('.card').offsetWidth + 20; // Card width + margin
let currentPosition = 0;
// Scroll to the next set of cards
nextBtn.addEventListener('click', () => {
currentPosition -= cardWidth;
if (currentPosition < -carousel.scrollWidth / 2) {
currentPosition = 0; // Reset to the start for infinite scrolling
}
carousel.style.transform = `translateX(${currentPosition}px)`;
});
// Scroll to the previous set of cards
prevBtn.addEventListener('click', () => {
currentPosition += cardWidth;
if (currentPosition > 0) {
currentPosition = -carousel.scrollWidth / 2; // Reset to the end for infinite scrolling
}
carousel.style.transform = `translateX(${currentPosition}px)`;
});
// Enable dragging for the carousel
let isDragging = false;
let startX, startScrollLeft;
carousel.addEventListener('mousedown', (e) => {
isDragging = true;
startX = e.pageX;
startScrollLeft = currentPosition;
});
carousel.addEventListener('mousemove', (e) => {
if (!isDragging) return;
e.preventDefault();
const x = e.pageX - startX;
currentPosition = startScrollLeft + x;
carousel.style.transform = `translateX(${currentPosition}px)`;
});
carousel.addEventListener('mouseup', () => {
isDragging = false;
});
carousel.addEventListener('mouseleave', () => {
isDragging = false;
});
</script>
</body>
</html>