javascript - How to prevent scrollLeft() from skipping values when scrolling too fast - Stack Overflow

admin2025-04-20  0

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>

Share Improve this question edited Mar 2 at 11:23 Itsjul1an asked Mar 2 at 9:48 Itsjul1anItsjul1an 3391 gold badge2 silver badges13 bronze badges 12
  • 1 You are right, I've updated the snippet – Itsjul1an Commented Mar 2 at 11:24
  • 2 @zer00ne Feel free to not comment at all if all you do is judge my code instead of trying to help. Quite annoying getting my hopes up after someone leaves a comment just to open it and read this... – Itsjul1an Commented Mar 2 at 12:59
  • 1 @zer00ne Didn't you read the earlier comments? Kiko pointed out it didn't work, turns out i made a typo (apparently that turns it into a 'code dump'), and I fixed it didn't I? So why are you even commenting on that 2 hours later? – Itsjul1an Commented Mar 2 at 14:34
  • 1 @RokoC.Buljan I'm afraid you're right, I guess another option would be to reduce the scrolled cards per swipe to 1, or disable scrolling altogether and implement arrows to scroll. Any ideas would be appreciated – Itsjul1an Commented Mar 2 at 14:36
  • 2 @Itsjul1an I was looking into the "scrollend" event (not yet available in Safari) and also IntersectionObserver API. But had not the time to create a smooth and seamless experience in a sane period od time. Transform is cool, but yes, you'll end up without the swipe feature. I would look at swiper.js library, also a nice read medium/web-dev-survey-from-kyoto/… which also has not the best solution but it covers a lot. PS: stackoverflow/a/72992704 – Roko C. Buljan Commented Mar 2 at 14:52
 |  Show 7 more comments

1 Answer 1

Reset to default -2
<!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">&#10094;</button>
      <button id="nextBtn">&#10095;</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>
转载请注明原文地址:http://conceptsofalgorithm.com/Algorithm/1745125768a286416.html

最新回复(0)