Stats Testimonial
Original · freetestimonials with trust stats
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-stats.json"use client";
import { useEffect, useRef, useState } from "react";
import {
animate,
AnimatePresence,
motion,
useInView,
useReducedMotion,
} from "motion/react";
type StatItem = {
value: number;
decimals?: number;
suffix: string;
label: string;
sub: string;
};
type Testimonial = {
id: string;
quote: string;
name: string;
role: string;
company: string;
initials: string;
gradient: string;
};
const STATS: StatItem[] = [
{
value: 4.9,
decimals: 1,
suffix: "/5",
label: "Average rating",
sub: "from 3,200+ verified reviews",
},
{
value: 98,
suffix: "%",
label: "Would recommend",
sub: "measured 90 days post-onboarding",
},
{
value: 12,
suffix: "k+",
label: "Teams onboarded",
sub: "across 40 countries",
},
{
value: 2.4,
decimals: 1,
suffix: "x",
label: "Faster to ship",
sub: "median cycle-time improvement",
},
];
const TESTIMONIALS: Testimonial[] = [
{
id: "priya",
quote:
"We cut our release cycle from every two weeks to daily. Cadence gave every squad the same source of truth, so the 'who's blocking whom' standups just stopped happening.",
name: "Priya Nair",
role: "VP Engineering",
company: "Series C fintech",
initials: "PN",
gradient: "from-indigo-500 to-violet-500",
},
{
id: "marcus",
quote:
"I've rolled out six tools that promised 'visibility.' This is the first one my team actually opens on a Monday. Onboarding took an afternoon, not a quarter.",
name: "Marcus Feld",
role: "Head of Operations",
company: "400-person logistics team",
initials: "MF",
gradient: "from-emerald-500 to-sky-500",
},
{
id: "elena",
quote:
"The reporting alone paid for the contract. I walked into a board meeting with live numbers instead of a slide deck I'd stitched together at midnight.",
name: "Elena Sokolova",
role: "Director of Product",
company: "D2C marketplace",
initials: "ES",
gradient: "from-rose-500 to-amber-500",
},
{
id: "david",
quote:
"Security review is usually where tools go to die. Cadence passed our SOC 2 checklist without a single exception, and the audit log saved us during a customer review.",
name: "David Okafor",
role: "CTO",
company: "Healthtech startup",
initials: "DO",
gradient: "from-sky-500 to-indigo-500",
},
{
id: "hana",
quote:
"We're spread across nine time zones. Async handoffs used to lose a full day. Now work moves while we sleep and nothing falls through the cracks.",
name: "Hana Kim",
role: "Chief of Staff",
company: "Remote-first agency",
initials: "HK",
gradient: "from-violet-500 to-rose-500",
},
];
function StarIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
className={className}
>
<path d="M12 2.25l2.955 5.99 6.61.96-4.783 4.662 1.129 6.585L12 17.297l-5.911 3.107 1.129-6.585L2.435 9.2l6.61-.96L12 2.25z" />
</svg>
);
}
function QuoteGlyph({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 48 48"
fill="currentColor"
aria-hidden="true"
className={className}
>
<path d="M18 10c-6.075 0-11 4.925-11 11v17h15V23h-8c0-2.761 2.239-5 5-5V10zm23 0c-6.075 0-11 4.925-11 11v17h15V23h-8c0-2.761 2.239-5 5-5V10z" />
</svg>
);
}
function ArrowIcon({
direction,
className,
}: {
direction: "left" | "right";
className?: string;
}) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
{direction === "left" ? (
<path d="M15 18l-6-6 6-6" />
) : (
<path d="M9 18l6-6-6-6" />
)}
</svg>
);
}
function ShieldCheckIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<path d="M12 3l7 3v5c0 4.5-3 8.5-7 10-4-1.5-7-5.5-7-10V6l7-3z" />
<path d="M9 12l2 2 4-4" />
</svg>
);
}
function Stat({
stat,
inView,
reduced,
}: {
stat: StatItem;
inView: boolean;
reduced: boolean;
}) {
const [display, setDisplay] = useState(reduced ? stat.value : 0);
useEffect(() => {
if (!inView) return;
if (reduced) {
setDisplay(stat.value);
return;
}
const controls = animate(0, stat.value, {
duration: 1.6,
ease: [0.16, 1, 0.3, 1],
onUpdate: (v) => setDisplay(v),
});
return () => controls.stop();
}, [inView, reduced, stat.value]);
const formatted = stat.decimals
? display.toFixed(stat.decimals)
: Math.round(display).toString();
return (
<div className="relative flex flex-col items-center rounded-2xl border border-slate-200/70 bg-white/60 px-4 py-6 text-center backdrop-blur-sm transition-colors dark:border-white/10 dark:bg-white/5">
<div className="flex items-baseline justify-center gap-0.5 font-semibold tracking-tight text-slate-900 dark:text-white">
<span className="text-4xl sm:text-5xl tstxstats-tabnum">
{formatted}
</span>
<span className="text-xl text-indigo-600 dark:text-indigo-400 sm:text-2xl">
{stat.suffix}
</span>
</div>
<div className="mt-2 text-sm font-medium text-slate-700 dark:text-slate-200">
{stat.label}
</div>
<div className="mt-1 text-xs text-slate-500 dark:text-slate-400">
{stat.sub}
</div>
</div>
);
}
function Avatar({
initials,
gradient,
size,
}: {
initials: string;
gradient: string;
size: "sm" | "lg";
}) {
const dim =
size === "lg" ? "h-14 w-14 text-lg" : "h-11 w-11 text-sm";
return (
<span
aria-hidden="true"
className={`inline-flex ${dim} shrink-0 items-center justify-center rounded-full bg-gradient-to-br ${gradient} font-semibold text-white shadow-sm ring-2 ring-white/70 dark:ring-white/10`}
>
{initials}
</span>
);
}
export default function TstxStats() {
const reduced = !!useReducedMotion();
const [active, setActive] = useState(0);
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
const statsRef = useRef<HTMLDivElement>(null);
const inView = useInView(statsRef, { once: true, margin: "-80px" });
const current = TESTIMONIALS[active];
function goTo(index: number) {
const count = TESTIMONIALS.length;
setActive(((index % count) + count) % count);
}
function onKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
const count = TESTIMONIALS.length;
let next = active;
if (event.key === "ArrowRight" || event.key === "ArrowDown") {
next = (active + 1) % count;
} else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
next = (active - 1 + count) % count;
} else if (event.key === "Home") {
next = 0;
} else if (event.key === "End") {
next = count - 1;
} else {
return;
}
event.preventDefault();
setActive(next);
tabRefs.current[next]?.focus();
}
const panelMotion = reduced
? { initial: false as const, animate: {}, exit: {} }
: {
initial: { opacity: 0, y: 14 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -14 },
};
return (
<section className="relative w-full overflow-hidden bg-slate-50 py-24 dark:bg-slate-950 sm:py-32">
<style>{`
@keyframes tstxstats-float {
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
50% { transform: translate3d(0, -22px, 0) scale(1.06); }
}
@keyframes tstxstats-drift {
0%, 100% { transform: translate3d(0, 0, 0); }
50% { transform: translate3d(24px, 14px, 0); }
}
.tstxstats-blob-a { animation: tstxstats-float 16s ease-in-out infinite; }
.tstxstats-blob-b { animation: tstxstats-drift 20s ease-in-out infinite; }
.tstxstats-tabnum { font-variant-numeric: tabular-nums; }
@media (prefers-reduced-motion: reduce) {
.tstxstats-blob-a,
.tstxstats-blob-b { animation: none; }
}
`}</style>
{/* Decorative background */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
<div className="tstxstats-blob-a absolute -left-24 top-8 h-72 w-72 rounded-full bg-indigo-400/25 blur-3xl dark:bg-indigo-600/20" />
<div className="tstxstats-blob-b absolute -right-20 bottom-0 h-80 w-80 rounded-full bg-violet-400/20 blur-3xl dark:bg-violet-600/20" />
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-slate-300/60 to-transparent dark:via-white/10" />
</div>
<div className="relative mx-auto max-w-6xl px-6">
{/* Header */}
<motion.div
initial={reduced ? false : { opacity: 0, y: 16 }}
whileInView={reduced ? {} : { opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
className="mx-auto max-w-2xl text-center"
>
<span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/70 px-3 py-1 text-xs font-medium text-slate-600 backdrop-blur-sm dark:border-white/10 dark:bg-white/5 dark:text-slate-300">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full rounded-full bg-emerald-500/70" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
</span>
Loved by teams that ship
</span>
<h2 className="mt-5 text-balance text-3xl font-semibold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
Don't take our word for it. Take theirs.
</h2>
<p className="mt-4 text-pretty text-base leading-relaxed text-slate-600 dark:text-slate-300 sm:text-lg">
Thousands of teams run their most important work on Cadence.
Here's what actually changed after they switched.
</p>
</motion.div>
{/* Trust stats */}
<div
ref={statsRef}
className="mt-14 grid grid-cols-2 gap-4 sm:gap-5 lg:grid-cols-4"
>
{STATS.map((stat) => (
<Stat
key={stat.label}
stat={stat}
inView={inView}
reduced={reduced}
/>
))}
</div>
{/* Featured testimonial */}
<div className="mt-14">
<div className="relative overflow-hidden rounded-3xl border border-slate-200/80 bg-white/70 p-8 shadow-sm backdrop-blur-sm dark:border-white/10 dark:bg-white/[0.04] sm:p-12">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 bg-gradient-to-br from-indigo-50/60 via-transparent to-violet-50/40 dark:from-indigo-500/[0.06] dark:to-violet-500/[0.06]"
/>
<QuoteGlyph className="pointer-events-none absolute right-8 top-8 h-16 w-16 text-indigo-200/70 dark:text-indigo-500/20" />
<div className="relative">
<div className="flex items-center gap-3">
<div className="flex text-amber-400">
{Array.from({ length: 5 }).map((_, i) => (
<StarIcon key={i} className="h-5 w-5" />
))}
</div>
<span className="inline-flex items-center gap-1 text-xs font-medium text-emerald-600 dark:text-emerald-400">
<ShieldCheckIcon className="h-4 w-4" />
Verified customer
</span>
</div>
<AnimatePresence mode="wait" initial={false}>
<motion.blockquote
key={current.id}
{...panelMotion}
transition={{ duration: reduced ? 0 : 0.4, ease: [0.16, 1, 0.3, 1] }}
id="tstx-panel"
role="tabpanel"
aria-labelledby={`tstx-tab-${current.id}`}
tabIndex={0}
className="mt-6 outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/50 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950"
>
<p className="text-balance text-xl font-medium leading-relaxed text-slate-800 dark:text-slate-100 sm:text-2xl">
“{current.quote}”
</p>
<footer className="mt-7 flex items-center gap-4">
<Avatar
initials={current.initials}
gradient={current.gradient}
size="lg"
/>
<div>
<div className="font-semibold text-slate-900 dark:text-white">
{current.name}
</div>
<div className="text-sm text-slate-600 dark:text-slate-400">
{current.role} · {current.company}
</div>
</div>
</footer>
</motion.blockquote>
</AnimatePresence>
</div>
</div>
{/* Controls */}
<div className="mt-6 flex flex-col items-center gap-4 sm:flex-row sm:justify-between">
<div className="order-2 flex items-center gap-2 sm:order-1">
<button
type="button"
onClick={() => goTo(active - 1)}
aria-label="Previous testimonial"
className="inline-flex h-10 w-10 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60 dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:hover:bg-white/10"
>
<ArrowIcon direction="left" className="h-5 w-5" />
</button>
<button
type="button"
onClick={() => goTo(active + 1)}
aria-label="Next testimonial"
className="inline-flex h-10 w-10 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60 dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:hover:bg-white/10"
>
<ArrowIcon direction="right" className="h-5 w-5" />
</button>
</div>
{/* Tabs / people selector */}
<div
role="tablist"
aria-label="Choose a customer testimonial"
onKeyDown={onKeyDown}
className="order-1 flex w-full flex-wrap items-center justify-center gap-2 sm:order-2 sm:w-auto sm:justify-end"
>
{TESTIMONIALS.map((t, i) => {
const selected = active === i;
return (
<button
key={t.id}
ref={(el) => {
tabRefs.current[i] = el;
}}
role="tab"
id={`tstx-tab-${t.id}`}
aria-selected={selected}
aria-controls="tstx-panel"
tabIndex={selected ? 0 : -1}
onClick={() => setActive(i)}
className={`group inline-flex items-center gap-2.5 rounded-full border px-2 py-1.5 pr-3.5 text-left transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60 ${
selected
? "border-indigo-300 bg-indigo-50 dark:border-indigo-500/40 dark:bg-indigo-500/10"
: "border-transparent bg-transparent hover:bg-slate-100 dark:hover:bg-white/5"
}`}
>
<Avatar
initials={t.initials}
gradient={t.gradient}
size="sm"
/>
<span className="hidden sm:block">
<span
className={`block text-sm font-medium ${
selected
? "text-slate-900 dark:text-white"
: "text-slate-600 dark:text-slate-300"
}`}
>
{t.name}
</span>
<span className="block text-xs text-slate-500 dark:text-slate-400">
{t.role}
</span>
</span>
</button>
);
})}
</div>
</div>
</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 →
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

Marquee Testimonial
Originaltestimonial marquee rows
Cards Avatar Testimonial
Originaltestimonial cards with avatars

