Web InnoventixFreeCode

Infinite Marquee Testimonial Wall

Original · free

A three-row marquee wall of review cards scrolling in alternating directions that pause on hover, with a shimmering gradient headline and faded edges.

byWeb InnoventixReact + Tailwind
testimonial marqueeinfinite scroll wallanimated review wallcss marquee reactpause on hover marqueeframer motion testimonials
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/atest-marquee-wall.json
atest-marquee-wall.tsx
"use client";

import { motion } from "motion/react";

type Review = {
  quote: string;
  name: string;
  role: string;
  initials: string;
  tint: string;
};

const rowA: Review[] = [
  { quote: "Cut our reporting time from days to minutes. Nobody misses the spreadsheets.", name: "Elena Ruiz", role: "COO, Cadence", initials: "ER", tint: "from-amber-500 to-orange-500" },
  { quote: "The single best tooling decision we have made this decade.", name: "Kofi Mensah", role: "Founder, Loomly", initials: "KM", tint: "from-rose-500 to-pink-500" },
  { quote: "It just quietly works, every single day, without drama.", name: "Anya Petrova", role: "CTO, Palisade", initials: "AP", tint: "from-orange-500 to-red-500" },
];

const rowB: Review[] = [
  { quote: "Support answered in minutes and actually solved the problem.", name: "Marcus Bell", role: "Ops, Kestrel", initials: "MB", tint: "from-fuchsia-500 to-rose-500" },
  { quote: "Our onboarding time dropped by two thirds in the first month.", name: "Sofia Lindqvist", role: "PM, Everly", initials: "SL", tint: "from-amber-500 to-yellow-500" },
  { quote: "Reporting our board finally trusts without a second glance.", name: "Idris Bello", role: "CFO, Meridian", initials: "IB", tint: "from-pink-500 to-rose-500" },
];

const rowC: Review[] = [
  { quote: "I keep recommending it to anyone who will sit still long enough.", name: "Grace Okonkwo", role: "Lead, Brightfold", initials: "GO", tint: "from-red-500 to-orange-500" },
  { quote: "Migrated three teams over a weekend and nothing broke.", name: "Tobias Reinhardt", role: "Eng Manager, Northwind", initials: "TR", tint: "from-orange-500 to-amber-500" },
  { quote: "Genuinely a pleasure to open first thing in the morning.", name: "Priya Nair", role: "Design, Loomly", initials: "PN", tint: "from-rose-500 to-red-500" },
];

function Card({ r, duplicate }: { r: Review; duplicate?: boolean }) {
  return (
    <figure
      aria-hidden={duplicate ? "true" : undefined}
      className="w-[19rem] shrink-0 rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm transition-transform duration-300 hover:-translate-y-1 dark:border-zinc-800 dark:bg-zinc-900"
    >
      <div className="flex items-center gap-0.5" aria-label="Rated 5 out of 5">
        {[0, 1, 2, 3, 4].map((s) => (
          <svg key={s} viewBox="0 0 24 24" className="h-4 w-4 fill-amber-400" aria-hidden="true">
            <path d="M11.48 3.5a.56.56 0 011.04 0l2.12 5.11a.56.56 0 00.48.35l5.52.44c.5.04.7.66.32.99l-4.2 3.6a.56.56 0 00-.19.56l1.29 5.38a.56.56 0 01-.84.61l-4.73-2.88a.56.56 0 00-.58 0l-4.73 2.88a.56.56 0 01-.84-.61l1.29-5.38a.56.56 0 00-.19-.56l-4.2-3.6a.56.56 0 01.32-.99l5.52-.44a.56.56 0 00.48-.35L11.48 3.5z" />
          </svg>
        ))}
      </div>
      <blockquote className="mt-3 text-[15px] leading-relaxed text-zinc-700 dark:text-zinc-300">
        {r.quote}
      </blockquote>
      <figcaption className="mt-4 flex items-center gap-3">
        <span
          className={`grid h-9 w-9 shrink-0 place-items-center rounded-full bg-gradient-to-br ${r.tint} text-xs font-semibold text-white`}
          aria-hidden="true"
        >
          {r.initials}
        </span>
        <span className="min-w-0">
          <span className="flex items-center gap-1 text-sm font-semibold text-zinc-900 dark:text-white">
            <span className="truncate">{r.name}</span>
            <svg viewBox="0 0 24 24" className="h-3.5 w-3.5 shrink-0 fill-sky-500" aria-hidden="true">
              <path d="M12 2l2.4 1.8 3 .1 1 2.8 2.4 1.8-.9 2.9.9 2.9-2.4 1.8-1 2.8-3 .1L12 22l-2.4-1.8-3-.1-1-2.8L3.2 15.5l.9-2.9-.9-2.9 2.4-1.8 1-2.8 3-.1L12 2zm-1 12.4l5-5-1.4-1.4-3.6 3.6-1.6-1.6L8 11.4l3 3z" />
            </svg>
          </span>
          <span className="block truncate text-xs text-zinc-500 dark:text-zinc-400">{r.role}</span>
        </span>
      </figcaption>
    </figure>
  );
}

function MarqueeRow({ items, direction, duration }: { items: Review[]; direction: "l" | "r"; duration: number }) {
  return (
    <div className="atest-mq group relative overflow-hidden">
      <div
        className="atest-mq-track flex w-max gap-5"
        style={{ animationName: direction === "l" ? "atest-mq-l" : "atest-mq-r", animationDuration: `${duration}s` }}
      >
        {[...items, ...items, ...items, ...items].map((r, i) => (
          <Card key={`${r.name}-${i}`} r={r} duplicate={i >= items.length} />
        ))}
      </div>
    </div>
  );
}

export default function AtestMarqueeWall() {
  return (
    <section className="relative overflow-hidden bg-zinc-50 px-6 py-20 dark:bg-zinc-950 md:py-28">
      <style>{`
        @keyframes atest-mq-l { from { transform: translateX(0); } to { transform: translateX(-50%); } }
        @keyframes atest-mq-r { from { transform: translateX(-50%); } to { transform: translateX(0); } }
        @keyframes atest-mq-shine { to { background-position: 200% center; } }
        .atest-mq-track { animation-timing-function: linear; animation-iteration-count: infinite; will-change: transform; }
        .atest-mq:hover .atest-mq-track { animation-play-state: paused; }
        .atest-mq-title {
          background-image: linear-gradient(90deg, #f59e0b, #f43f5e, #f59e0b);
          background-size: 200% auto;
          -webkit-background-clip: text;
          background-clip: text;
          color: transparent;
          animation: atest-mq-shine 6s linear infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .atest-mq-track { animation: none; }
          .atest-mq-title { animation: none; }
        }
      `}</style>

      <motion.div
        initial={{ opacity: 0, y: 18 }}
        whileInView={{ opacity: 1, y: 0 }}
        viewport={{ once: true, amount: 0.5 }}
        transition={{ duration: 0.5, ease: "easeOut" }}
        className="relative mx-auto max-w-2xl text-center"
      >
        <span className="inline-flex items-center gap-2 rounded-full border border-amber-200 bg-amber-50 px-4 py-1.5 text-xs font-medium text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-400">
          <svg viewBox="0 0 24 24" className="h-3.5 w-3.5 fill-current" aria-hidden="true">
            <path d="M11.48 3.5a.56.56 0 011.04 0l2.12 5.11a.56.56 0 00.48.35l5.52.44c.5.04.7.66.32.99l-4.2 3.6a.56.56 0 00-.19.56l1.29 5.38a.56.56 0 01-.84.61l-4.73-2.88a.56.56 0 00-.58 0l-4.73 2.88a.56.56 0 01-.84-.61l1.29-5.38a.56.56 0 00-.19-.56l-4.2-3.6a.56.56 0 01.32-.99l5.52-.44a.56.56 0 00.48-.35L11.48 3.5z" />
          </svg>
          4.9 average from 2,300 reviews
        </span>
        <h2 className="mt-5 text-3xl font-bold tracking-tight text-balance sm:text-4xl">
          <span className="text-zinc-900 dark:text-white">A wall that never stops </span>
          <span className="atest-mq-title">talking</span>
        </h2>
        <p className="mt-4 text-lg text-zinc-600 dark:text-zinc-400">
          Hover any row to pause and read. Every card is a real team, in their own words.
        </p>
      </motion.div>

      <motion.div
        initial={{ opacity: 0 }}
        whileInView={{ opacity: 1 }}
        viewport={{ once: true, amount: 0.1 }}
        transition={{ duration: 0.6, ease: "easeOut" }}
        className="relative mt-14 flex flex-col gap-5 [mask-image:linear-gradient(to_right,transparent,black_8%,black_92%,transparent)]"
      >
        <MarqueeRow items={rowA} direction="l" duration={38} />
        <MarqueeRow items={rowB} direction="r" duration={46} />
        <MarqueeRow items={rowC} direction="l" duration={42} />
      </motion.div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →
Three Card Testimonial Grid

Three Card Testimonial Grid

Original

A clean three-column grid of testimonial cards, each with a star rating, quote and an initial avatar with name and role.

Single Featured Quote

Single Featured Quote

Original

A large centred pull quote with a decorative quotation mark, five-star rating and a prominent author avatar for a single standout testimonial.

Logo Wall Marquee

Logo Wall Marquee

Original

A pair of infinitely scrolling, pause-on-hover marquees pairing a customer logo wall with compact quote cards, with reduced-motion support.

Masonry Testimonial Wall

Masonry Testimonial Wall

Original

A CSS-columns masonry wall of testimonial cards in varied lengths, each with a star rating and initial avatar for a natural, organic layout.

Aggregate Rating Spotlight

Aggregate Rating Spotlight

Original

A split layout pairing a large aggregate rating score, star row and overlapping avatar stack with two featured review cards.

Auto-Rotating Quote Carousel

Auto-Rotating Quote Carousel

Original

An auto-advancing testimonial carousel that slides and crossfades between quotes with a live progress bar, pause-on-hover, and dot plus arrow navigation.

Staggered Reveal Testimonial Grid

Staggered Reveal Testimonial Grid

Original

A testimonial grid whose cards spring in with a staggered scroll reveal and light up with an animated gradient border on hover.

Spotlight Hero

Spotlight Hero

Original

A centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero

Split Hero

Original

A two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero

Gradient Spotlight Hero

Original

A minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

App Preview Hero

App Preview Hero

Original

A centred hero that pairs headline copy with a realistic product dashboard mock built entirely from markup, complete with browser chrome and a floating notification card.

Waitlist Capture Hero

Waitlist Capture Hero

Original

A dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.