Infinite Marquee
Seamless auto-scrolling strip for logos, testimonials, or tags — pauses on hover, directional, and gradient-masked edges
marqueecarousellogosinfinitescrolling
Preview
Source Code
"use client";
import { Children, ReactNode, useId } from "react";
interface InfiniteMarqueeProps {
children: ReactNode;
direction?: "left" | "right";
speed?: number;
pauseOnHover?: boolean;
gap?: number;
className?: string;
}
export default function InfiniteMarquee({
children,
direction = "left",
speed = 30,
pauseOnHover = true,
gap = 48,
className = "",
}: InfiniteMarqueeProps) {
const uid = useId().replace(/:/g, "");
const animationName = `marquee-${uid}`;
const items = Children.toArray(children);
// Direction is achieved by flipping the keyframes' end transform sign.
const endX = direction === "left" ? "-50%" : "0%";
const startX = direction === "left" ? "0%" : "-50%";
const styles = `
@keyframes ${animationName} {
from { transform: translateX(${startX}); }
to { transform: translateX(${endX}); }
}
.${animationName}-track {
display: flex;
width: max-content;
flex-shrink: 0;
gap: ${gap}px;
padding-right: ${gap}px;
animation: ${animationName} ${speed}s linear infinite;
will-change: transform;
}
.${animationName}-root:hover .${animationName}-track {
animation-play-state: ${pauseOnHover ? "paused" : "running"};
}
@media (prefers-reduced-motion: reduce) {
.${animationName}-track { animation-duration: ${speed * 4}s; }
}
`;
return (
<div
className={`${animationName}-root relative overflow-hidden ${className}`}
style={{
maskImage:
"linear-gradient(to right, transparent, black 12%, black 88%, transparent)",
WebkitMaskImage:
"linear-gradient(to right, transparent, black 12%, black 88%, transparent)",
}}
>
<style dangerouslySetInnerHTML={{ __html: styles }} />
<div className={`${animationName}-track`}>
{items.map((child, i) => (
<div key={`a-${i}`} className="flex shrink-0 items-center">
{child}
</div>
))}
{items.map((child, i) => (
<div key={`b-${i}`} aria-hidden className="flex shrink-0 items-center">
{child}
</div>
))}
</div>
</div>
);
}