Web InnoventixFreeCode

Marquee Testimonial

Original · free

testimonial marquee rows

byWeb InnoventixReact + Tailwind
tstxmarqueetestimonials
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/tstx-marquee.json
tstx-marquee.tsx
"use client";

import { useState } from "react";
import { motion, useReducedMotion } from "motion/react";

type Testimonial = {
  name: string;
  role: string;
  quote: string;
  gradient: string;
};

const GRADIENTS = [
  "from-indigo-500 to-violet-600",
  "from-emerald-500 to-sky-600",
  "from-rose-500 to-amber-500",
  "from-sky-500 to-indigo-600",
  "from-violet-500 to-rose-500",
  "from-amber-500 to-rose-600",
];

const TESTIMONIALS: Testimonial[] = [
  {
    name: "Maya Okafor",
    role: "Head of Growth, DTC brand",
    quote:
      "We cut weekly reporting from six hours to about twenty minutes. Flowbeam pulls every channel into one board, so Monday standup finally starts on time.",
    gradient: GRADIENTS[0],
  },
  {
    name: "Devon Reyes",
    role: "Engineering Manager",
    quote:
      "The thing that sold my team was the audit trail. Every dashboard change is versioned, so when a number looks off we can see exactly who touched what.",
    gradient: GRADIENTS[1],
  },
  {
    name: "Priya Nandakumar",
    role: "Founder, design studio",
    quote:
      "I run a five-person studio and I don't have a data team. Flowbeam is the first tool that didn't make me feel like I needed one.",
    gradient: GRADIENTS[2],
  },
  {
    name: "Liam Hughes",
    role: "VP Operations, logistics",
    quote:
      "We replaced three separate subscriptions with this. The migration took an afternoon, and support actually answered on a Saturday.",
    gradient: GRADIENTS[3],
  },
  {
    name: "Sofia Marchetti",
    role: "Product Lead, B2B SaaS",
    quote:
      "Our activation rate climbed 18% once we could finally see where users dropped off. Not a growth hack — we could just read the funnel clearly.",
    gradient: GRADIENTS[4],
  },
  {
    name: "Marcus Bello",
    role: "Data Analyst",
    quote:
      "I've used the expensive enterprise stuff. This gets me 90% of the way there for a fraction of the price, and the query builder is honestly nicer.",
    gradient: GRADIENTS[5],
  },
  {
    name: "Hana Kim",
    role: "CMO, fintech startup",
    quote:
      "Board decks used to eat my Friday. Now I export a live view and the numbers are never stale. My CFO stopped emailing me screenshots.",
    gradient: GRADIENTS[0],
  },
  {
    name: "Tomás Ferreira",
    role: "Freelance consultant",
    quote:
      "I onboard new clients in an hour instead of a week. I duplicate a template, connect their accounts, and it's live before the kickoff call ends.",
    gradient: GRADIENTS[1],
  },
  {
    name: "Amara Diallo",
    role: "Head of CX, marketplace",
    quote:
      "Tickets tied to confusing metrics basically disappeared. When the whole company reads the same dashboard, people stop arguing about whose number is right.",
    gradient: GRADIENTS[2],
  },
  {
    name: "Noah Feldman",
    role: "Startup founder",
    quote:
      "We were flying blind on retention. Two weeks in, Flowbeam surfaced a churn spike we'd have missed for a quarter. It paid for itself immediately.",
    gradient: GRADIENTS[3],
  },
  {
    name: "Layla Haddad",
    role: "Marketing Director, hospitality",
    quote:
      "The alerts are the quiet hero. I get a ping when bookings dip below trend, so I'm reacting in hours instead of at the end of the month.",
    gradient: GRADIENTS[4],
  },
  {
    name: "Ethan Walsh",
    role: "COO, edtech",
    quote:
      "Rollout across 40 people was painless. No training deck, no help-desk queue. People just started using it. That never happens with internal tools.",
    gradient: GRADIENTS[5],
  },
];

function initials(name: string): string {
  return name
    .split(" ")
    .slice(0, 2)
    .map((w) => w.charAt(0))
    .join("")
    .toUpperCase();
}

function Stars() {
  return (
    <div className="flex gap-0.5" aria-hidden="true">
      {Array.from({ length: 5 }).map((_, i) => (
        <svg key={i} viewBox="0 0 20 20" className="h-4 w-4 fill-amber-400">
          <path d="M10 1.6l2.55 5.17 5.7.83-4.12 4.02.97 5.68L10 14.6l-5.1 2.7.97-5.68L1.75 7.6l5.7-.83z" />
        </svg>
      ))}
    </div>
  );
}

function TestimonialCard({ t }: { t: Testimonial }) {
  return (
    <figure className="flex h-full w-[300px] shrink-0 flex-col gap-4 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-shadow duration-300 hover:shadow-lg hover:shadow-indigo-500/5 dark:border-slate-800 dark:bg-slate-900/70 dark:hover:shadow-indigo-400/10 sm:w-[360px]">
      <div className="flex items-center justify-between">
        <Stars />
        <svg
          viewBox="0 0 24 24"
          aria-hidden="true"
          className="h-7 w-7 fill-slate-200 dark:fill-slate-700"
        >
          <path d="M10 6c-3.3 0-6 2.7-6 6v6h6v-6H7c0-1.7 1.3-3 3-3V6zm10 0c-3.3 0-6 2.7-6 6v6h6v-6h-3c0-1.7 1.3-3 3-3V6z" />
        </svg>
      </div>
      <blockquote className="text-[15px] leading-relaxed text-slate-700 dark:text-slate-200">
        &ldquo;{t.quote}&rdquo;
      </blockquote>
      <figcaption className="mt-auto flex items-center gap-3 pt-2">
        <span
          className={`flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-gradient-to-br text-sm font-semibold text-white ${t.gradient}`}
          aria-hidden="true"
        >
          {initials(t.name)}
        </span>
        <span className="min-w-0">
          <span className="block truncate text-sm font-semibold text-slate-900 dark:text-white">
            {t.name}
          </span>
          <span className="block truncate text-xs text-slate-500 dark:text-slate-400">
            {t.role}
          </span>
        </span>
      </figcaption>
    </figure>
  );
}

function MarqueeRow({
  items,
  direction,
  duration,
}: {
  items: Testimonial[];
  direction: "left" | "right";
  duration: number;
}) {
  return (
    <div className="tstxmq-row group relative flex overflow-x-auto">
      <ul
        className="tstxmq-track flex w-max items-stretch py-1"
        data-dir={direction}
        style={{ animationDuration: `${duration}s` }}
      >
        {items.map((t, i) => (
          <li key={`a-${i}`} className="mr-5 flex">
            <TestimonialCard t={t} />
          </li>
        ))}
        {items.map((t, i) => (
          <li key={`b-${i}`} className="mr-5 flex" aria-hidden="true">
            <TestimonialCard t={t} />
          </li>
        ))}
      </ul>
    </div>
  );
}

export default function TstxMarquee() {
  const reduce = useReducedMotion();
  const [paused, setPaused] = useState(false);

  const row1 = TESTIMONIALS.slice(0, 4);
  const row2 = TESTIMONIALS.slice(4, 8);
  const row3 = TESTIMONIALS.slice(8, 12);

  return (
    <section
      className="relative w-full overflow-hidden bg-slate-50 px-4 py-24 dark:bg-slate-950 sm:px-6 sm:py-28 lg:px-8"
      data-tstxmq-paused={paused ? "true" : "false"}
      aria-labelledby="tstxmq-heading"
    >
      <style>{`
        @keyframes tstxmq-left {
          from { transform: translateX(0); }
          to { transform: translateX(-50%); }
        }
        @keyframes tstxmq-right {
          from { transform: translateX(-50%); }
          to { transform: translateX(0); }
        }
        .tstxmq-track {
          animation-timing-function: linear;
          animation-iteration-count: infinite;
          will-change: transform;
        }
        .tstxmq-track[data-dir="left"] { animation-name: tstxmq-left; }
        .tstxmq-track[data-dir="right"] { animation-name: tstxmq-right; }
        .tstxmq-row { scrollbar-width: none; -ms-overflow-style: none; }
        .tstxmq-row::-webkit-scrollbar { display: none; }
        .tstxmq-row:hover .tstxmq-track,
        .tstxmq-row:focus-within .tstxmq-track { animation-play-state: paused; }
        [data-tstxmq-paused="true"] .tstxmq-track { animation-play-state: paused !important; }
        @media (prefers-reduced-motion: reduce) {
          .tstxmq-track { animation: none !important; }
        }
      `}</style>

      {/* Ambient background wash */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute -top-24 left-1/2 h-72 w-72 -translate-x-1/2 rounded-full bg-indigo-400/20 blur-3xl dark:bg-indigo-500/10"
      />
      <div
        aria-hidden="true"
        className="pointer-events-none absolute -bottom-24 right-10 h-72 w-72 rounded-full bg-emerald-400/15 blur-3xl dark:bg-emerald-500/10"
      />

      {/* Header */}
      <motion.div
        initial={reduce ? false : { opacity: 0, y: 22 }}
        whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
        viewport={{ once: true, margin: "-80px" }}
        transition={{ duration: 0.6, ease: "easeOut" }}
        className="relative mx-auto max-w-2xl text-center"
      >
        <span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-medium text-slate-600 shadow-sm dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300">
          <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
          Loved by teams that live in the data
        </span>
        <h2
          id="tstxmq-heading"
          className="mt-5 text-balance text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl md:text-5xl"
        >
          The dashboard your whole team actually opens
        </h2>
        <p className="mx-auto mt-4 max-w-xl text-pretty text-base leading-relaxed text-slate-600 dark:text-slate-400">
          Over 4,000 teams use Flowbeam to turn scattered spreadsheets into one
          board everyone trusts. Here&rsquo;s what a few of them told us.
        </p>
        <div className="mt-6 flex items-center justify-center gap-3">
          <Stars />
          <span className="text-sm font-medium text-slate-700 dark:text-slate-300">
            4.9 out of 5
            <span className="text-slate-400 dark:text-slate-500">
              {" "}
              · 1,200+ verified reviews
            </span>
          </span>
        </div>
      </motion.div>

      {/* Marquee */}
      <div
        className="relative mt-14"
        role="region"
        aria-label="Customer testimonials, auto-scrolling"
      >
        {/* Pause / play control */}
        <div className="mx-auto mb-6 flex max-w-7xl justify-center sm:justify-end sm:px-2">
          <button
            type="button"
            onClick={() => setPaused((p) => !p)}
            aria-pressed={paused}
            className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm transition-colors hover:bg-slate-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-950"
          >
            {paused ? (
              <svg viewBox="0 0 20 20" aria-hidden="true" className="h-4 w-4 fill-current">
                <path d="M6 4.5v11l9-5.5-9-5.5z" />
              </svg>
            ) : (
              <svg viewBox="0 0 20 20" aria-hidden="true" className="h-4 w-4 fill-current">
                <path d="M6 4h3v12H6zM11 4h3v12h-3z" />
              </svg>
            )}
            <span>{paused ? "Play testimonials" : "Pause testimonials"}</span>
          </button>
        </div>

        <div className="flex flex-col gap-5">
          <MarqueeRow items={row1} direction="left" duration={55} />
          <MarqueeRow items={row2} direction="right" duration={70} />
          <MarqueeRow items={row3} direction="left" duration={62} />
        </div>

        {/* Edge fades */}
        <div
          aria-hidden="true"
          className="pointer-events-none absolute inset-y-0 left-0 z-10 w-16 bg-gradient-to-r from-slate-50 to-transparent dark:from-slate-950 sm:w-28"
        />
        <div
          aria-hidden="true"
          className="pointer-events-none absolute inset-y-0 right-0 z-10 w-16 bg-gradient-to-l from-slate-50 to-transparent dark:from-slate-950 sm:w-28"
        />
      </div>

      {/* Trust footer */}
      <motion.p
        initial={reduce ? false : { opacity: 0 }}
        whileInView={reduce ? undefined : { opacity: 1 }}
        viewport={{ once: true }}
        transition={{ duration: 0.6, delay: 0.1 }}
        className="relative mx-auto mt-14 max-w-xl text-center text-sm text-slate-500 dark:text-slate-400"
      >
        Every review is from a verified, paying customer. Hover a row to pause it,
        or use the button above.
      </motion.p>
    </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.

Infinite Marquee Testimonial Wall

Infinite Marquee Testimonial Wall

Original

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

Grid Testimonial

Grid Testimonial

Original

testimonial grid

Single Large Testimonial

Single Large Testimonial

Original

single large quote

Cards Avatar Testimonial

Cards Avatar Testimonial

Original

testimonial cards with avatars

Logos Quote Testimonial

Logos Quote Testimonial

Original

quote with a logo strip