Falnix UI

Infinite Moving Cards

A seamless, auto-scrolling marquee effect perfect for testimonials, brand logos, or gallery items.

InteractiveScroll

Component Preview

  • It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity.
    Charles DickensA Tale of Two Cities
  • To be, or not to be, that is the question: Whether 'tis nobler in the mind to suffer The slings and arrows of outrageous fortune, Or to take arms against a sea of troubles And by opposing end them?
    William ShakespeareHamlet
  • All that we see or seem is but a dream within a dream.
    Edgar Allan PoeA Dream Within a Dream
  • It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.
    Jane AustenPride and Prejudice
  • Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world.
    Herman MelvilleMoby-Dick

Installation

npm install framer-motion clsx tailwind-merge

Copy the source code below into components/ui/infinite-moving-cards.tsx:

1"use client";
2
3import { cn } from "@falnix/ui";
4import React, { useEffect, useState } from "react";
5
6export const InfiniteMovingCards = ({
7 items,
8 direction = "left",
9 speed = "fast",
10 pauseOnHover = true,
11 className,
12}: {
13 items: {
14 quote: string;
15 name: string;
16 title: string;
17 }[];
18 direction?: "left" | "right";
19 speed?: "fast" | "normal" | "slow";
20 pauseOnHover?: boolean;
21 className?: string;
22}) => {
23 const containerRef = React.useRef<HTMLDivElement>(null);
24 const scrollerRef = React.useRef<HTMLUListElement>(null);
25
26 useEffect(() => {
27 addAnimation();
28 }, []);
29
30 const [start, setStart] = useState(false);
31 function addAnimation() {
32 if (containerRef.current && scrollerRef.current) {
33 const scrollerContent = Array.from(scrollerRef.current.children);
34
35 scrollerContent.forEach((item) => {
36 const duplicatedItem = item.cloneNode(true);
37 if (scrollerRef.current) {
38 scrollerRef.current.appendChild(duplicatedItem);
39 }
40 });
41
42 getDirection();
43 getSpeed();
44 setStart(true);
45 }
46 }
47 const getDirection = () => {
48 if (containerRef.current) {
49 if (direction === "left") {
50 containerRef.current.style.setProperty(
51 "--animation-direction",
52 "forwards"
53 );
54 } else {
55 containerRef.current.style.setProperty(
56 "--animation-direction",
57 "reverse"
58 );
59 }
60 }
61 };
62 const getSpeed = () => {
63 if (containerRef.current) {
64 if (speed === "fast") {
65 containerRef.current.style.setProperty("--animation-duration", "20s");
66 } else if (speed === "normal") {
67 containerRef.current.style.setProperty("--animation-duration", "40s");
68 } else {
69 containerRef.current.style.setProperty("--animation-duration", "80s");
70 }
71 }
72 };
73 return (
74 <div
75 ref={containerRef}
76 className={cn(
77 "scroller relative z-20 max-w-7xl overflow-hidden mask-[linear-gradient(to_right,transparent,white_20%,white_80%,transparent)]",
78 className
79 )}
80 >
81 <ul
82 ref={scrollerRef}
83 className={cn(
84 " flex min-w-full shrink-0 gap-4 py-4 w-max flex-nowrap",
85 start && "animate-scroll ",
86 pauseOnHover && "hover:[animation-play-state:paused]"
87 )}
88 >
89 {items.map((item, idx) => (
90 <li
91 className="w-[350px] max-w-full relative rounded-2xl border border-b-0 flex-shrink-0 border-slate-700 px-8 py-6 md:w-[450px]"
92 style={{
93 background:
94 "linear-gradient(180deg, var(--slate-800), var(--slate-900)",
95 }}
96 key={item.name}
97 >
98 <blockquote>
99 <div
100 aria-hidden="true"
101 className="user-select-none -z-1 pointer-events-none absolute -left-0.5 -top-0.5 h-[calc(100%_+_4px)] w-[calc(100%_+_4px)]"
102 ></div>
103 <span className=" relative z-20 text-sm leading-[1.6] text-gray-100 font-normal">
104 {item.quote}
105 </span>
106 <div className="relative z-20 mt-6 flex flex-row items-center">
107 <span className="flex flex-col gap-1">
108 <span className=" text-sm leading-[1.6] text-gray-400 font-normal">
109 {item.name}
110 </span>
111 <span className=" text-sm leading-[1.6] text-gray-400 font-normal">
112 {item.title}
113 </span>
114 </span>
115 </div>
116 </blockquote>
117 </li>
118 ))}
119 </ul>
120 </div>
121 );
122};

Props & Customization

PropTypeDefaultDescription
itemsArray<Item>requiredArray of objects containing quote, name, and title.
direction"left" | "right""left"Direction of the scroll animation.
speed"fast" | "normal" | "slow""fast"Speed of the animation.
pauseOnHoverbooleantrueWhether to pause the animation on hover.