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();