Vertical Testimonial Marquee
Original · freeThree vertical testimonial columns scroll at different speeds and opposite directions, pausing on hover with a shine sweep so reviews loop endlessly beside a sticky heading.
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/marquee-testimonial-columns.json"use client";
import { motion } from "motion/react";
type Testimonial = {
quote: string;
name: string;
role: string;
initials: string;
tint: string;
};
const testimonials: Testimonial[] = [
{
quote: "We replaced three separate tools in a single afternoon. The team barely noticed the switch, which is exactly what you want.",
name: "Priya Nadar",
role: "Head of Ops, Loomly",
initials: "PN",
tint: "from-indigo-500 to-violet-500",
},
{
quote: "Support answered in four minutes at midnight. That kind of reliability is why we renewed for three years.",
name: "Marcus Feld",
role: "CTO, Palisade",
initials: "MF",
tint: "from-emerald-500 to-teal-500",
},
{
quote: "Our reporting used to take a full day. Now the board gets numbers they trust before their first coffee.",
name: "Ada Okafor",
role: "CFO, Meridian",
initials: "AO",
tint: "from-rose-500 to-orange-500",
},
{
quote: "Onboarding dropped from two weeks to two days. New hires ship real work in their first sprint.",
name: "Liam Whitfield",
role: "Eng Lead, Cadence",
initials: "LW",
tint: "from-sky-500 to-cyan-500",
},
{
quote: "It just quietly works every single day. I have honestly forgotten the last time something broke on us.",
name: "Sofia Delgado",
role: "Founder, Brightfold",
initials: "SD",
tint: "from-amber-500 to-yellow-500",
},
{
quote: "The migration I dreaded turned out to be the easiest project of the quarter. Zero downtime, no drama.",
name: "Kenji Aoyama",
role: "VP Product, Kestrel",
initials: "KA",
tint: "from-fuchsia-500 to-pink-500",
},
];
function Stars() {
return (
<div className="flex items-center gap-0.5" aria-label="Rated 5 out of 5 stars">
{[0, 1, 2, 3, 4].map((s) => (
<svg key={s} viewBox="0 0 24 24" className="h-3.5 w-3.5 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>
);
}
function Card({ t, dupe }: { t: Testimonial; dupe?: boolean }) {
return (
<figure
aria-hidden={dupe ? true : undefined}
className="group/card relative overflow-hidden rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:border-indigo-300 hover:shadow-lg dark:border-white/10 dark:bg-zinc-900/60 dark:hover:border-indigo-400/40"
>
<span className="pointer-events-none absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/50 to-transparent transition-transform duration-700 group-hover/card:translate-x-full dark:via-white/10" />
<div className="relative flex items-center justify-between">
<Stars />
<svg viewBox="0 0 24 24" className="h-6 w-6 fill-zinc-200 dark:fill-zinc-700" aria-hidden="true">
<path d="M7 7h4v6a4 4 0 01-4 4v-2a2 2 0 002-2H7zm8 0h4v6a4 4 0 01-4 4v-2a2 2 0 002-2h-2z" />
</svg>
</div>
<blockquote className="relative mt-3 text-sm leading-relaxed text-zinc-700 dark:text-zinc-300">
{t.quote}
</blockquote>
<figcaption className="relative mt-4 flex items-center gap-3">
<span className={`inline-flex h-9 w-9 items-center justify-center rounded-full bg-gradient-to-br ${t.tint} text-xs font-bold text-white shadow-sm`}>
{t.initials}
</span>
<span className="flex flex-col">
<span className="text-sm font-semibold text-zinc-900 dark:text-white">{t.name}</span>
<span className="text-xs text-zinc-500 dark:text-zinc-400">{t.role}</span>
</span>
</figcaption>
</figure>
);
}
function Column({
items,
dir,
duration,
className = "",
}: {
items: Testimonial[];
dir: "up" | "down";
duration: number;
className?: string;
}) {
return (
<div className={`group relative overflow-hidden ${className}`}>
<div
className={`tc-col tc-${dir} gap-4`}
style={{ animationDuration: `${duration}s` }}
>
{[...items, ...items].map((t, i) => (
<Card key={`${dir}-${i}`} t={t} dupe={i >= items.length} />
))}
</div>
</div>
);
}
export default function MarqueeTestimonialColumns() {
const colA = [testimonials[0], testimonials[3], testimonials[5]];
const colB = [testimonials[1], testimonials[4], testimonials[2]];
const colC = [testimonials[2], testimonials[0], testimonials[4]];
return (
<section className="relative overflow-hidden bg-zinc-50 px-6 py-20 dark:bg-zinc-950 md:py-28">
<style>{`
@keyframes tc-up { from { transform: translateY(0); } to { transform: translateY(-50%); } }
@keyframes tc-down { from { transform: translateY(-50%); } to { transform: translateY(0); } }
.tc-col { display: flex; flex-direction: column; width: 100%; animation-timing-function: linear; animation-iteration-count: infinite; will-change: transform; }
.tc-up { animation-name: tc-up; }
.tc-down { animation-name: tc-down; }
.group:hover .tc-col { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
.tc-col { animation: none; }
}
`}</style>
<div aria-hidden="true" className="pointer-events-none absolute -top-24 left-1/2 h-96 w-[42rem] -translate-x-1/2 rounded-full bg-gradient-to-br from-indigo-400/20 via-fuchsia-400/10 to-transparent blur-3xl dark:from-indigo-600/20 dark:via-fuchsia-600/10" />
<div className="relative mx-auto grid max-w-6xl gap-12 lg:grid-cols-[minmax(0,20rem)_minmax(0,1fr)] lg:gap-10">
<div className="lg:sticky lg:top-24 lg:self-start">
<motion.span
initial={{ opacity: 0, y: 14 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
className="text-sm font-semibold uppercase tracking-widest text-indigo-600 dark:text-indigo-400"
>
Loved by builders
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 18 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.55, delay: 0.08, ease: [0.22, 1, 0.36, 1] }}
className="mt-4 text-balance text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl dark:text-white"
>
The reviews keep rolling in
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 18 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.55, delay: 0.16, ease: [0.22, 1, 0.36, 1] }}
className="mt-4 text-lg text-zinc-600 dark:text-zinc-400"
>
Real words from teams who put us to work every day. Hover over any column to pause and read.
</motion.p>
<motion.div
initial={{ opacity: 0, y: 18 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.55, delay: 0.24, ease: [0.22, 1, 0.36, 1] }}
className="mt-8 flex items-center gap-4"
>
<div className="flex items-center gap-1">
<span className="text-3xl font-bold text-zinc-900 dark:text-white">4.9</span>
<Stars />
</div>
<span className="h-8 w-px bg-zinc-300 dark:bg-zinc-700" />
<span className="text-sm text-zinc-600 dark:text-zinc-400">
from <span className="font-semibold text-zinc-900 dark:text-white">2,400+</span> reviews
</span>
</motion.div>
</div>
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.6, delay: 0.2, ease: [0.22, 1, 0.36, 1] }}
className="relative grid h-[34rem] grid-cols-1 gap-4 [-webkit-mask-image:linear-gradient(to_bottom,transparent,black_12%,black_88%,transparent)] [mask-image:linear-gradient(to_bottom,transparent,black_12%,black_88%,transparent)] sm:grid-cols-2 lg:grid-cols-3"
aria-label="Customer testimonials"
>
<Column items={colA} dir="up" duration={28} />
<Column items={colB} dir="down" duration={34} />
<Column items={colC} dir="up" duration={40} className="hidden lg:block" />
</motion.div>
</div>
</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 →
Glowing Logo Marquee
OriginalA dual-row infinite logo marquee whose rows scroll in opposite directions, pause on hover and lift each logo with a shimmer sweep over an animated gradient glow.

Velocity Band Marquee
OriginalA skewed dual-direction headline marquee of big gradient and outlined words plus a pill ticker, with speed and pause controls and pause on hover over an animated mesh background.

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

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

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

App Preview Hero
OriginalA 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
OriginalA dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.

Image Backdrop Hero
OriginalA cinematic full-bleed hero with a layered image-style backdrop, overlaid left-aligned copy and a trusted-by logos strip.

Gradient Mesh Hero with Staggered Text Reveal
OriginalA full-screen hero pairing a drifting animated gradient-mesh backdrop with a word-by-word staggered blur-in headline and a logo marquee under the fold.

Typewriter Rotating Words Hero
OriginalA hero whose headline cycles through rotating words with a live typewriter caret over an animated grid backdrop, degrading to a gentle word swap when reduced motion is on.

Floating UI Cards Hero with Mouse Parallax
OriginalA hero with glassy floating UI cards that drift continuously and shift on mouse-parallax depth around a staggered centre headline.

Aurora Hero with Shimmer CTA and Logo Marquee
OriginalA dark hero with animated aurora light beams, a shimmering sweep across the primary CTA, and dual-direction logo marquees beneath the fold.

