Docs
Infinite Moving Cards

Infinite Moving Cards

This InfiniteMovingCards component creates a continuously scrolling carousel of testimonials or quotes with a modern, elegant design.

  • Having been closely involved in the development of Unizoy UI, I can confidently say it’s a game‑changer for modern web projects. Its GSAP‑powered animations, React component structure, and TypeScript foundation make building sleek, high‑performance UIs a seamless experience. It’s not just a library, it’s a toolkit built with developers in mind.
    Ankush JaiswalFull Stack Developer
  • Haven't found any better open-source GSAP component library!
    Bhavya PatelUnizoy
  • Just copy and paste, and boom, your animated website is ready! I love how easy it is to create animations using Unizoy UI.
    Rajpurohit VijeshUnizoy
  • Unizoy UI is very simple to use. I can add nice animations without writing too much code. It saves my time and works really well.
    Faizan PathanFull Stack Developer
  • A useful library for teams looking to build beautiful, animated website without added complexity.
    Neel PatelInstant branding

Install the following dependencies:

pnpm add gsap @gsap/react

Make a file and copy paste this code in a file with name infinite-moving-cards.tsx

"use client"
import { cn } from "@/lib/utils"
import { useRef } from "react"
import gsap from "gsap"
import { useGSAP } from "@gsap/react"
 
export function InfiniteMovingCards({
  items,
  direction = "left",
  speed = "fast",
  pauseOnHover = true,
  className,
}: {
  items: {
    quote: string
    name: string
    title: string
  }[]
  direction?: "left" | "right"
  speed?: "fast" | "normal" | "slow"
  pauseOnHover?: boolean
  className?: string
}) {
  const containerRef = useRef<HTMLDivElement>(null)
  const scrollerRef = useRef<HTMLUListElement>(null)
 
  // Register GSAP with React
  gsap.registerPlugin(useGSAP)
 
  useGSAP(
    () => {
      if (!scrollerRef.current) return
 
      // Clone items for seamless looping
      const scrollerContent = Array.from(scrollerRef.current.children)
      scrollerContent.forEach((item) => {
        const duplicatedItem = item.cloneNode(true)
        scrollerRef.current?.appendChild(duplicatedItem)
      })
 
      // Get dimensions
      const itemWidth = scrollerContent[0].getBoundingClientRect().width
      const totalItemsWidth = itemWidth * scrollerContent.length
 
      // Set animation duration based on speed
      let duration = 40 // normal speed
      if (speed === "fast") duration = 20
      else if (speed === "slow") duration = 80
 
      // Create a timeline for the continuous animation
      const tl = gsap.timeline({
        repeat: -1, // Infinite repeat
        defaults: { ease: "none" },
      })
 
      if (direction === "left") {
        // Initial position
        gsap.set(scrollerRef.current, { x: 0 })
 
        // Create the main animation
        tl.to(scrollerRef.current, {
          x: -totalItemsWidth,
          duration,
          onComplete: () => {
            // Reset the position when the animation completes
            gsap.set(scrollerRef.current, { x: 0 })
          },
        })
      } else {
        // Initial position for right direction
        gsap.set(scrollerRef.current, { x: -totalItemsWidth })
 
        // Create the animation for right direction
        tl.to(scrollerRef.current, {
          x: 0,
          duration,
          onComplete: () => {
            // Reset the position when the animation completes
            gsap.set(scrollerRef.current, { x: -totalItemsWidth })
          },
        })
      }
 
      // Setup hover pause functionality
      if (pauseOnHover && containerRef.current) {
        containerRef.current.addEventListener("mouseenter", () => tl.pause())
        containerRef.current.addEventListener("mouseleave", () => tl.play())
      }
 
      // Cleanup is handled automatically by useGSAP
      return () => {
        tl.kill() // For extra safety
      }
    },
    { scope: containerRef, dependencies: [direction, speed, pauseOnHover] }
  )
 
  return (
    <div
      ref={containerRef}
      className={cn(
        "relative z-20 max-w-7xl overflow-hidden",
        "[mask-image:linear-gradient(to_right,transparent,white_20%,white_80%,transparent)]",
        className
      )}
    >
      <ul
        ref={scrollerRef}
        className="flex w-max min-w-full shrink-0 flex-nowrap gap-4 py-4"
        style={{ willChange: "transform" }} // Optimization for animations
      >
        {items.map((item, idx) => (
          <li
            className="relative w-[350px] max-w-full shrink-0 rounded-2xl border border-b-0 border-zinc-200 bg-[linear-gradient(180deg,#fafafa,#f5f5f5)] px-8 py-6 md:w-[450px] dark:border-zinc-700 dark:bg-[linear-gradient(180deg,#27272a,#18181b)]"
            key={idx}
          >
            <blockquote>
              <div
                aria-hidden="true"
                className="user-select-none pointer-events-none absolute -top-0.5 -left-0.5 -z-1 h-[calc(100%_+_4px)] w-[calc(100%_+_4px)]"
              ></div>
              <span className="relative z-20 text-sm leading-[1.6] font-normal text-neutral-800 dark:text-gray-100">
                {item.quote}
              </span>
              <div className="relative z-20 mt-6 flex flex-row items-center">
                <span className="flex flex-col gap-1">
                  <span className="text-sm leading-[1.6] font-normal text-neutral-500 dark:text-gray-400">
                    {item.name}
                  </span>
                  <span className="text-sm leading-[1.6] font-normal text-neutral-500 dark:text-gray-400">
                    {item.title}
                  </span>
                </span>
              </div>
            </blockquote>
          </li>
        ))}
      </ul>
    </div>
  )
}

Props

NameTypeDescription
items*ArrayArray of objects containing quote, name, and title that will be displayed in the moving cards. Each object should have the shape: {quote: string, name: string, title: string}
directionstringThe direction in which the cards will scroll. Options are "left" or "right". Default is "left".
speedstringThe scrolling speed of the cards. Options are "fast", "normal", or "slow". Default is "fast".
pauseOnHoverbooleanWhether the animation should pause when the user hovers over the component. Default is true.
classNamestringAdditional CSS class names to apply to the container element for custom styling. Optional.

Note: Props marked with * are required.