JSFiddle - React, Tailwind, and code Playground

by Jordan Sayner

HTML

<div class="carousel-demo">
  <button class="arrow arrow--left" aria-label="Previous">‹</button>

  <ul class="nike-carousel" id="carousel">
    <!-- Simple demo cards -->
    <li class="nike-product-card">
      <div class="card">
        <img src="https://picsum.photos/seed/1/480/320" alt="">
        <h3>Card 1</h3>
      </div>
    </li>
    <li class="nike-product-card">
      <div class="card">
        <img src="https://picsum.photos/seed/2/480/320" alt="">
        <h3>Card 2</h3>
      </div>
    </li>
    <li class="nike-product-card">
      <div class="card">
        <img src="https://picsum.photos/seed/3/480/320" alt="">
        <h3>Card 3</h3>
      </div>
    </li>
    <li class="nike-product-card">
      <div class="card">
        <img src="https://picsum.photos/seed/4/480/320" alt="">
        <h3>Card 4</h3>
      </div>
    </li>
    <li class="nike-product-card">
      <div class="card">
        <img src="https://picsum.photos/seed/5/480/320" alt="">
        <h3>Card 5</h3>
      </div>
    </li>
  </ul>

  <button class="arrow arrow--right" aria-label="Next">›</button>
</div>

CSS

/* From the article: scrolling container + cards */
.nike-carousel {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding: 0 18px; /* keeps cards off the bezel */
  gap: 18px;
  padding: 0 18px;
  list-style: none;
  margin: 0;
}

.nike-product-card {
  flex-shrink: 0;
  scroll-snap-align: start; /* snap target at the start edge */
}

/* Demo layout & styles (not required by the article, just to look nice) */
.carousel-demo {
  position: relative;
  max-width: 920px;
  margin: 32px auto;
  padding: 0 48px; /* space for arrows */
}

.card {
  width: 280px;
  border-radius: 12px;
  overflow: hidden;
  background: #111;
  color: #fff;
  box-shadow: 0 10px 30px rgba(0,0,0,.15);
}
.card img {
  display: block;
  width: 100%;
  height: auto;
}
.card h3 {
  margin: 12px 12px 16px;
  font: 600 16px/1.2 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}

/* Arrows */
.arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  inline-size: 40px;
  block-size: 40px;
  border-radius: 999px;
  border: 0;
  background: #fff;
  box-shadow: 0 6px 18px rgba(0,0,0,.15);
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
  display: grid;
  place-items: center;
}
.arrow--left  { left: 0; }
.arrow--right { right: 0; }

.arrow:disabled {
  opacity: .35;
  cursor: not-allowed;
}

/* Hide default scrollbar but keep scroll functionality (optional) */
.nike-carousel {
  scrollbar-width: none;          /* Firefox */
}
.nike-carousel::-webkit-scrollbar { display: none; } /* WebKit */

JavaScript

// Vanilla JS port of the article's React logic:
// - check button enable/disable based on scroll position
// - scroll by exactly one card using scrollIntoView({ behavior:'smooth', inline:'start' })

const el = document.getElementById('carousel');
const btnPrev = document.querySelector('.arrow--left');
const btnNext = document.querySelector('.arrow--right');

function checkScrollButtons() {
  const { scrollLeft, scrollWidth, clientWidth } = el;
  // 5px grace so the buttons flip cleanly (from the article’s idea)
  btnPrev.disabled = scrollLeft <= 5;
  btnNext.disabled = scrollLeft >= scrollWidth - clientWidth - 5;
}

function scrollByOne(direction) {
  const children = Array.from(el.children);
  if (!children.length) return;

  const cardWidth = children[0].offsetWidth || 1; // assume uniform widths
  const currentIndex = Math.round(el.scrollLeft / cardWidth);
  const targetIndex = direction === 'left'
    ? Math.max(0, currentIndex - 1)
    : Math.min(children.length - 1, currentIndex + 1);

  children[targetIndex].scrollIntoView({
    behavior: 'smooth',
    block: 'nearest',
    inline: 'start' // honors scroll-padding
  });
}

// Wire up events
btnPrev.addEventListener('click', () => scrollByOne('left'));
btnNext.addEventListener('click', () => scrollByOne('right'));
el.addEventListener('scroll', checkScrollButtons);

// Initialize
checkScrollButtons();