Building Infinite Animated Scroll for React components
Tags: webdev, frontend, typescript, animation
Published: 2024-02-04 18:43:24

**Contents**    
1. [General idea](#1.-general-idea)
2. [Lining up the components](#2.-lining-up-the-components)
3. [Animating](#3.-animating)     
4. [Restarting](#4.-restarting)
5. [Adjusting](#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](https://www.npmjs.com/package/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](https://react-components-slideshow.vercel.app/)

![infinite scroll animation](https://cdn.sanity.io/images/l7j5p3xz/production/4eca1f3c02de56c8a30a82a36ac3160b445a2dd0-1200x664.gif)

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:

```typescript
// 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>
  );
...
```

> **NOTE**: [NextUI cards](https://nextui.org/) are used here to aid with UX.


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

```typescript
// 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>
  );
...
```

This should result in something that looks like this:

![cards with scroll](https://cdn.sanity.io/images/l7j5p3xz/production/886abcc0d891a153c080c984a77e9e13d5dd1051-2520x1395.png)


## 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:


```typescript
// 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));
}
...
```


## 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:

```typescript
// 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));
    }
}
...
```

![infinite jumping animation](https://cdn.sanity.io/images/l7j5p3xz/production/90e5acc98ba8b7f6f565ded387828a85765d578f-1200x665.gif)

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.

```typescript
// 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>
  );
...
```

> **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:

```typescript
// src/App.tsx
...
const OFFSET = 23;
const INTERVAL = CARD_WIDTH + SPACING + OFFSET;
...
```

![infinite scroll animation](https://cdn.sanity.io/images/l7j5p3xz/production/4eca1f3c02de56c8a30a82a36ac3160b445a2dd0-1200x664.gif)

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

[GitHub source code](https://github.com/Snoblomma/react-components-slideshow)
[DEMO](https://react-components-slideshow.vercel.app/)

    
Happy coding!    

&nbsp;

Let's connect on my [Portfolio](https://katepurchel.com/) | [GitHub](https://github.com/Snoblomma/) | [Codepen](https://codepen.io/snoblomma)