I'm trying to create an infinite scrolling horizontal carousel of icons using React and Framer Motion. The animation works, but I'm encountering issues with visible empty space at the end of the animation loop.
Here's my current component:
import { motion } from "framer-motion";
import { AwardsContainer, AwardsImg } from "./Awards.styled";
const textAnimate = {
initial: { x: 0 },
animate: {
x: "-55%",
transition: {
repeat: Infinity,
repeatType: "mirror",
duration: 25,
},
},
};
function Awards() {
const repeatedAwards = [...awardsData, ...awardsData];
return (
<AwardsContainer>
<motion.div variants={textAnimate} initial="initial" animate="animate">
{repeatedAwards.map(({ id, src, alt }, index) => (
<AwardsImg key={`${id}-${index}`} src={src} alt={alt} />
))}
</motion.div>
</AwardsContainer>
);
}
export default Awards;
Problem
Even when I double or triple the awardsData
, the animation eventually ends or shows an empty space (especially on large screens like TVs or ultra-wide monitors).
I also tried dynamically measuring the screen width with the react-use-measure
library to calculate how many copies of awardsData
I need to append. But this approach still results in visible gaps at the end during the scroll loop.
What I Need Help With How can I create a truly seamless, infinite horizontal scroll for a list of icons (or any items) using Framer Motion (or alternative methods in React) — without gaps at the end regardless of screen size?
The common approach is to use two copies of identical data, such as icons, that you want to show in a carousel. The main thing to keep in mind is that the length of a single copy should be equal to or larger than the screen size. Here is the piece of code that I reuse for horizontal carousels. It uses tailwind v4, and is easily replicable in CSS.
Tailwind Config:
@import "tailwindcss";
@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--animate-marquee: marquee 20s linear infinite;
@keyframes marquee {
0% {
transform: translateX(0%);
}
100% {
transform: translateX(-100%);
}
}
}
The component below imports a list of icons and creates two similar copies of them
export default function Carousel({ logos }: {logos: {link: string}[]}) {
return (
<div className=" w-full mt-12 relative flex overflow-x-hidden bg-scrollerColor">
<div className="animate-marquee py-6 flex whitespace-nowrap flex-shrink-0">
{logos.map((logo, index) => (
<img
key={`A${index}`}
src={logo.link}
className="h-12 w-12 md:h-16 md:w-16 rounded-full bg-gray-200 mx-4"
alt="Coin Logo"
/>
))}
</div>
<div className="animate-marquee top-0 py-6 flex whitespace-nowrap flex-shrink-0">
{logos.map((logo, index) => (
<img
key={`B${index}`}
src={logo.link}
className="h-12 w-12 md:h-16 md:w-16 rounded-full bg-gray-200 mx-4"
alt="Coin Logo"
/>
))}
</div>
</div>
);
}
You can check this out in the live demo: