DEV Community

Cover image for Building Infinite Animated Scroll for React components
Kate
Kate

Posted on • Originally published at katepurchel.com

Building Infinite Animated Scroll for React components

Contents

  1. General idea
  2. Lining up the components
  3. Animating
  4. Restarting
  5. Adjusting

If you ever needed to present your components in an endless animation scroll, you might have found the variety of existing libraries like React Swipe.

Sometimes, however, we want to build something simple from scratch.

Let's say we want to showcase the new products added to our website. We want them to be ready visible however the total amount would overflow the page. This is the idea behind using infinite animated scroll.

We also want for:

  • animation to start on page load
  • components to be presented running in an endless loop

1. General idea

When talking about infinite components presentation, we might at first consider that we will have to re-render our slider as the animation progresses - by adding new elements to the end of the list and removing from the beginning.

This, as you might already guessed, could be detrimental for performance of the app so we will have to think of something else.

The best solution to date, in my opinion, is the usage of illusion and a bit of coding. We will also programmatically double the amount of the elements in the scroll to allow plenty of overlap. Then, we will simply reset starting position when the start of the duplicate is reached.

You can find working demo here

infinite scroll animation

Let's now break down the code to see how exactly it works.

2. Lining up the components

First of all, let's add our container component:

// src/App.tsx
...
return (
    <NextUIProvider style={{ height: '100%' }}>
      <div ref={wrapper} className='flex h-full items-center'>
        <div ref={container} className={`flex p-8 overflow-hidden`}>
        </div>
      </div>
    </NextUIProvider>
  );
...
Enter fullscreen mode Exit fullscreen mode

NOTE: NextUI cards are used here to aid with UX.

Now let's fill in the box with the cards:

// src/App.tsx

...
const CARD_WIDTH = 160;
const SPACING = 32;

// list of cards:
const list = [
...
]

...
return (
    <NextUIProvider style={{ height: '100%' }}>
      <div ref={wrapper} className='flex h-full items-center'>
        <div ref={container} className={`flex p-8 overflow-hidden`} style={{ gap: `${SPACING}px` }}>
                {list.map((item, index) => (
            <Card shadow="sm" key={index} style={{ minWidth: `${CARD_WIDTH}px` }}>
              <CardBody className="overflow-visible p-0">
                <Image
                  shadow="sm"
                  radius="lg"
                  width="100%"
                  alt={item.title}
                  className="w-full object-cover h-[140px]"
                  src={item.img}
                />
              </CardBody>
              <CardFooter className="text-small justify-between">
                <b>{item.title}</b>
                <p className="text-default-500">{item.price}</p>
              </CardFooter>
            </Card>
          ))}
        </div>
      </div>
    </NextUIProvider>
  );
...
Enter fullscreen mode Exit fullscreen mode

This should result in something that looks like this:

cards with scroll

3. Animating

Now that we have the desired box with overflowing cards, we want for it to start scrolling on component initialisation.

For this example we want it to scroll fixed distance, let's say interval = card width + padding.

We've going to use scrollBy to do so:

// src/App.tsx
...
const INTERVAL = CARD_WIDTH + SPACING;
...
async function scrollRight() {
      const scrollLeft = container.current?.scrollLeft;
      const clientWidth = container.current?.clientWidth;
      const scrollWidth = container.current?.scrollWidth;
      if (scrollLeft !== undefined && clientWidth !== undefined && scrollWidth !== undefined) {
         container.current?.scrollBy({
            left: INTERVAL,
            behavior: 'smooth',
          })
      }
      await new Promise(r => setTimeout(r, 1500));
}
...
Enter fullscreen mode Exit fullscreen mode

4. Restarting

That looks better! However we still have not achieved the desired result - obviously the amount of cards is finite and the animation stops.

Let's now add a loop to make sure our animation restarts. Whenever our scroll nears the end, we will use setScroll to reset its position:

// src/App.tsx
...
async function scrollRight() {
    while (true) {
      const scrollLeft = container.current?.scrollLeft;
      const clientWidth = container.current?.clientWidth;
      const scrollWidth = container.current?.scrollWidth;
            if (scrollLeft !== undefined && clientWidth !== undefined && scrollWidth !== undefined) {
          if (scrollLeft >= (scrollWidth - clientWidth)) {
            container.current?.scrollTo({
              left: 0,
              behavior: 'instant'
            })
          }
          else {
            container.current?.scrollBy({
              left: INTERVAL,
              behavior: 'smooth',
            })
          }
      }
      await new Promise(r => setTimeout(r, 1500));
    }
}
...
Enter fullscreen mode Exit fullscreen mode

infinite jumping animation

While this works, it obviously doesn't create smooth transition.

This is where we have to use our duplication trick. We're going to double amount of elements with list.concat(list), and reset scroll position when reaching the start of the duplicate array.

// src/App.tsx
...
async function scrollRight() {
    while (true) {
      const scrollLeft = container.current?.scrollLeft;
      const clientWidth = container.current?.clientWidth;
      const scrollWidth = container.current?.scrollWidth;
      if (scrollLeft !== undefined && clientWidth !== undefined && scrollWidth !== undefined) {
        if (scrollLeft > 8 * (CARD_WIDTH + SPACING)) {
          container.current?.scrollTo({
            left: scrollLeft - 8 * (CARD_WIDTH + SPACING),
            behavior: 'instant'
          })
        }
        else {
          container.current?.scrollBy({
            left: INTERVAL,
            behavior: 'smooth',
          })
        }
      }
      await new Promise(r => setTimeout(r, 1500));
    }
  }
...
return (
    <NextUIProvider style={{ height: '100%' }}>
      <div ref={wrapper} className='flex h-full items-center'>
        <div ref={container} className={`flex p-8 overflow-hidden`} style={{ gap: `${SPACING}px` }}>
          {list.concat(list).map((item, index) => (
                        ...
          ))}
        </div>
      </div>
    </NextUIProvider>
  );
...
Enter fullscreen mode Exit fullscreen mode

NOTE: Notice the behavior: 'instant' set in scroll options - it will set stroll instantly which will make for seamless transition

5. Adjusting

You can notice now that there's always some padding at the beginning of the row present. Even when animation is smooth, it still doesn't look quite natural.

We can fix this by introducing OFFSET value to add to our starting point. This will add some perceived variability to our scroll and make it look more "natural". For the OFFSET, any value between 0 and width of the card should work just fine:

// src/App.tsx
...
const OFFSET = 23;
const INTERVAL = CARD_WIDTH + SPACING + OFFSET;
...
Enter fullscreen mode Exit fullscreen mode

infinite scroll animation

That's it! You can see it in action below:

GitHub source code
DEMO

Happy coding!

 

Let's connect on my Portfolio | GitHub | Codepen

Top comments (0)