Contents
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
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>
);
...
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>
);
...
This should result in something that looks like this:
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));
}
...
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));
}
}
...
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>
);
...
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;
...
That's it! You can see it in action below:
Happy coding!
Top comments (0)