10/16 demo: Build apps faster with AI and your design stack

What are best AI tools? Take the State of AI survey

Builder.io
Builder.io
Contact sales
‹ Back to blog

AI

Build buttery smooth carousels with pure CSS like Nike

October 3, 2025

Written By Steve Sewell

How does Nike make these buttery smooth carousels on their website with pure CSS?

Let me show you how I quickly recreated this and how the code works behind it:

First, we need some markup

The first thing we are going to need is some markup that we can add the scroll behavior to.

Instead of hand coding all of it, there is an easier way. I used the Builder.io Chrome extension to copy the layout right to my clipboard to recreate it.

Builder is connected to an existing site I have, using my design system and all that good stuff, but you can generate net new ones too.

Now that we have some baseline markup, lets jump into the quick and easy way to get what we want.

There are two secrets you need: need CSS scroll snapping, and a method called scrollIntoView in JavaScript.

The easiest way to do this is just to prompt an LLM. In Builder.io I just selected the section I imported and typed the prompt:

add CSS scroll snapping, and add arrow buttons using scrollIntoView()

Now, using our magic words, we have beautiful snapping scroll that also moves one card at a time with arrows, just like Nike. On mobile, it behaves exactly how we want too.

  • CSS scroll-snap gives us native, hardware accelerated scrolling and snapping
  • scrollIntoView() cooperates with snapping and respects scroll-padding
  • No scroll jacking, no custom wheel handlers, just smooth native behavior

Let’s now break down the code. We’ll separate the container and the cards so the role of each line is clear.

.nike-carousel {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding: 0 18px;
}
  • display: flex lays the cards in a horizontal row
  • overflow-x: auto enables native horizontal scrolling
  • scroll-snap-type: x mandatory locks the final position to snap points on the X axis
  • scroll-padding: 0 18px offsets the snap target from the edges so cards do not hug the bezel
.nike-product-card {
  flex-shrink: 0;
  scroll-snap-align: start;
}
  • flex-shrink: 0 prevents width compression so snapping math stays stable
  • scroll-snap-align: start makes each card the snap target relative to the container start. You can try center or end for a different feel, but start plus padding matches Nike’s look

Now, if you want to add arrows to go to the next or previous items, we can use a handy method in JavaScript called scrollIntoView()

We host left and right arrows, the scrollable list, and keep button states in sync with scroll. I will show the full shape first, then fill in the scroll function.

import React, { useRef, useState, useEffect } from "react";

function ScrollableCarousel({ cards }) {
  const scrollRef = useRef<HTMLUListElement | null>(null);
  const [canScrollLeft, setCanScrollLeft] = useState(false);
  const [canScrollRight, setCanScrollRight] = useState(true);

  const checkScrollButtons = () => {
    /* filled in below */
  };

  const scrollByOne = (dir: "left" | "right") => {
    /* filled in below */
  };

  return (
    <div className="carousel-container">
      <LeftArrow onClick={() => scrollByOne("left")} disabled={!canScrollLeft} />
      <RightArrow onClick={() => scrollByOne("right")} disabled={!canScrollRight} />

      <ul
        ref={scrollRef}
        onScroll={checkScrollButtons}
        className="nike-carousel"
      >
        {cards.map((card, i) => (
          <Card card={card} key={1} />
        ))}
      </ul>
    </div>
  );
}

What the shell is doing: scrollRef lets us read scroll position and children. Button states come from scrollLeft, scrollWidth, and clientWidth, with a tiny 5px grace on each edge so the buttons disable cleanly. onScroll keeps states updated during drag or momentum scrolling.

This is the simple way that cooperates with snapping and respects your scroll-padding.

const scrollByOne = (dir: "left" | "right") => {
  const el = scrollRef.current;
  if (!el) return;

  const children = Array.from(el.children) as HTMLElement[];
  if (!children.length) return;

  const cardWidth = children[0].offsetWidth || 1;
  const currentIndex = Math.round(el.scrollLeft / cardWidth);

  const targetIndex =
    dir === "left"
      ? Math.max(0, currentIndex - 1)
      : Math.min(children.length - 1, currentIndex + 1);

  children[targetIndex].scrollIntoView({
    behavior: "smooth",
    block: "nearest",  // do not move vertically
    inline: "start",   // align to container start, honors scroll-padding
  });
};

Why I like this: we let the browser do the heavy lifting. Snap alignment stays consistent whether the user swipes, wheels, or clicks the arrows.

We need to enable or disable the left and right arrows based on where the user is in the scroll area. checkScrollButtons reads three native properties from the scroll container and sets two booleans.

const checkScrollButtons = () => {
  const el = scrollRef.current;
  if (!el) return;

  const { scrollLeft, scrollWidth, clientWidth } = el;

  // small grace so the buttons flip cleanly
  setCanScrollLeft(scrollLeft > 5);
  setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 5);
};

What each value means

scrollLeft is how many pixels the container is scrolled from the left.

scrollWidth is the total scrollable content width, including offscreen content.

clientWidth is the visible width of the container.

Once we have our code in place, we can use AI to play with variations. I like to use Builder.io to give me a visual canvas to rapidly experiment with and get a feel for each option.

For instance, I experimented with shifting more than one card at a time on scroll, with the prompt:

When I click any of the scroll arrow buttons scroll by 2 instead of 1

After trying that, I quite liked it. Felt like I was moving more like a page at a time like a normal carousel than just one tiny card at a time (I want to see more than just one more card over!)

Now that this is reproduced in Builder.io, we have an AI canvas to experiment with modifications. I can say: always scroll by two instead of one when I click these buttons, and see the feel instantly. This kind of visual IDE connected to your code lets you rapidly test what feels best.

The best part here with Builder is we are editing live production code in a branch. Designers can make UX tweaks directly in the visual canvas, then click Send PR.

Engineering gets a clean, well formatted pull request. If you want anything different, you can leave comments tagging the Builder.io bot. Say move this to a new file or extract the scroll logic into a hook.

The bot replies and pushes updates until you are ready to merge. This removes red lines and back and forth so engineers do not have to chase tiny alignment details and designers are not locked to static mocks.

Try Builder.io out for free and lmk your feedback!

Generate clean code using your components & design tokens
Try FusionGet a demo

Share

Twitter / X
LinkedIn
Facebook
Share this blog
Copy icon
Twitter "X" icon
LinkedIn icon
Facebook icon

Visually edit your codebase with AI

Using simple prompts or Figma-like controls.

Try it nowGet a demo

Design to Code Automation

A pragmatic guide for engineering leaders and development teams

Access Now

Continue Reading
AI7 MIN
Recreating Apple-style 3D scroll animations in Three.js and WebGL
October 2, 2025
AI6 MIN
Create a 3d scrolling animation with GSAP and Veo 3
October 1, 2025
AI6 MIN
Faster UX Design with AI
October 1, 2025