Testimonial Marquee

Two infinite rows of review cards moving in opposite directions.

Loved by product teams

“We rebuilt our marketing site in a weekend. The motion details make it feel premium without any extra work.”
PRPriya RamanHead of Design, Lumora
“Clean, typed components that drop straight into our Next.js app. No fighting with abstractions.”
DODaniel OkaforCTO, Stackline
“Our conversion rate went up 18% after switching to the new landing page. Customers notice the polish.”
EFElena FischerFounder, Northwave
“We rebuilt our marketing site in a weekend. The motion details make it feel premium without any extra work.”
PRPriya RamanHead of Design, Lumora
“Clean, typed components that drop straight into our Next.js app. No fighting with abstractions.”
DODaniel OkaforCTO, Stackline
“Our conversion rate went up 18% after switching to the new landing page. Customers notice the polish.”
EFElena FischerFounder, Northwave
“Finally a library that respects accessibility and performance while still looking this good.”
MLMarcus LeeEngineer, Orbital
“From idea to live page in an afternoon. It's become the first thing we reach for.”
SASofia AlvarezPM, Vertexa
“The spacing, typography and timing are all considered. It saves us days on every launch.”
KWKenji WatanabeDesign Lead, Cubeform
“Finally a library that respects accessibility and performance while still looking this good.”
MLMarcus LeeEngineer, Orbital
“From idea to live page in an afternoon. It's become the first thing we reach for.”
SASofia AlvarezPM, Vertexa
“The spacing, typography and timing are all considered. It saves us days on every launch.”
KWKenji WatanabeDesign Lead, Cubeform

Installation

  1. Install the dependencies

    $ npm i motion lucide-react
  2. Copy the source into components/ui/testimonial-marquee.tsx

    components/ui/testimonial-marquee.tsx
    "use client";
    
    import { motion } from "motion/react";
    import { Star } from "lucide-react";
    
    type Testimonial = { name: string; role: string; quote: string };
    
    const testimonials: Testimonial[] = [
      { name: "Priya Raman", role: "Head of Design, Lumora", quote: "We rebuilt our marketing site in a weekend. The motion details make it feel premium without any extra work." },
      { name: "Daniel Okafor", role: "CTO, Stackline", quote: "Clean, typed components that drop straight into our Next.js app. No fighting with abstractions." },
      { name: "Elena Fischer", role: "Founder, Northwave", quote: "Our conversion rate went up 18% after switching to the new landing page. Customers notice the polish." },
      { name: "Marcus Lee", role: "Engineer, Orbital", quote: "Finally a library that respects accessibility and performance while still looking this good." },
      { name: "Sofia Alvarez", role: "PM, Vertexa", quote: "From idea to live page in an afternoon. It's become the first thing we reach for." },
      { name: "Kenji Watanabe", role: "Design Lead, Cubeform", quote: "The spacing, typography and timing are all considered. It saves us days on every launch." },
    ];
    
    const gradients = ["from-blue-500 to-indigo-500", "from-emerald-500 to-teal-500", "from-amber-500 to-orange-500", "from-pink-500 to-rose-500", "from-violet-500 to-purple-500", "from-sky-500 to-cyan-500"];
    
    export function TestimonialCard({ t, index }: { t: Testimonial; index: number }) {
      const initials = t.name.split(" ").map((p) => p[0]).join("");
      return (
        <figure className="w-80 shrink-0 rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm">
          <div className="flex gap-0.5 text-amber-400">
            {Array.from({ length: 5 }).map((_, i) => (
              <Star key={i} className="size-4 fill-current" />
            ))}
          </div>
          <blockquote className="mt-3 text-sm leading-relaxed text-zinc-700">&ldquo;{t.quote}&rdquo;</blockquote>
          <figcaption className="mt-5 flex items-center gap-3">
            <span className={`grid size-9 place-items-center rounded-full bg-gradient-to-br text-xs font-semibold text-white ${gradients[index % gradients.length]}`}>
              {initials}
            </span>
            <span>
              <span className="block text-sm font-medium text-zinc-900">{t.name}</span>
              <span className="block text-xs text-zinc-500">{t.role}</span>
            </span>
          </figcaption>
        </figure>
      );
    }
    
    function Row({ items, reverse = false, duration = 40 }: { items: Testimonial[]; reverse?: boolean; duration?: number }) {
      return (
        <div className="flex overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]">
          <motion.div
            className="flex shrink-0 gap-4 pr-4"
            animate={{ x: reverse ? ["-50%", "0%"] : ["0%", "-50%"] }}
            transition={{ duration, repeat: Infinity, ease: "linear" }}
          >
            {[...items, ...items].map((t, i) => (
              <TestimonialCard key={i} t={t} index={i} />
            ))}
          </motion.div>
        </div>
      );
    }
    
    export function TestimonialMarquee({ items = testimonials }: { items?: Testimonial[] }) {
      const half = Math.ceil(items.length / 2);
      return (
        <div className="flex w-full flex-col gap-4">
          <Row items={items.slice(0, half)} />
          <Row items={items.slice(half)} reverse duration={46} />
        </div>
      );
    }
    
    export default function Demo() {
      return (
        <section className="w-full max-w-5xl">
          <h2 className="mb-10 text-center text-3xl font-semibold tracking-tight text-zinc-900">Loved by product teams</h2>
          <TestimonialMarquee />
        </section>
      );
    }
    
  3. Use it

    The file exports the component and a Demo showing typical usage. Import whichever you need.