Marquee Testimonial
Original · freetestimonial marquee rows
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"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">
“{t.quote}”
</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’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
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 quoteSimilar components
Browse all →
Three Card Testimonial Grid
OriginalA clean three-column grid of testimonial cards, each with a star rating, quote and an initial avatar with name and role.

Single Featured Quote
OriginalA 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
OriginalA pair of infinitely scrolling, pause-on-hover marquees pairing a customer logo wall with compact quote cards, with reduced-motion support.

Masonry Testimonial Wall
OriginalA 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
OriginalA split layout pairing a large aggregate rating score, star row and overlapping avatar stack with two featured review cards.

Auto-Rotating Quote Carousel
OriginalAn 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
OriginalA 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
OriginalA 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
Originaltestimonial grid

Single Large Testimonial
Originalsingle large quote
Cards Avatar Testimonial
Originaltestimonial cards with avatars

Logos Quote Testimonial
Originalquote with a logo strip

