Single Large Testimonial
Original · freesingle large quote
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-single-large.json"use client";
import { useRef, useState } from "react";
import type { KeyboardEvent, ReactNode } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Testimonial = {
id: string;
quote: string;
highlight: string;
name: string;
role: string;
initials: string;
avatar: string;
};
const TESTIMONIALS: Testimonial[] = [
{
id: "priya",
quote:
"We cut our monthly close from eleven days to three. The first time finance and ops read the same numbers without arguing over whose spreadsheet was right, I knew Beacon had already paid for itself.",
highlight: "eleven days to three",
name: "Priya Nadesan",
role: "VP of Finance, B2B logistics scale-up",
initials: "PN",
avatar: "from-indigo-500 to-violet-500",
},
{
id: "marcus",
quote:
"I stopped dreading Monday reporting. Beacon flags the one metric that's quietly drifting before it turns into a fire, so my team spends the week fixing problems instead of hunting for them.",
highlight: "before it turns into a fire",
name: "Marcus Ochoa",
role: "Head of Growth, consumer subscription app",
initials: "MO",
avatar: "from-emerald-500 to-sky-500",
},
{
id: "dana",
quote:
"Onboarding took an afternoon, not a quarter. Six weeks in, every manager builds their own dashboards and I haven't fielded a single 'can you pull this for me' request since.",
highlight: "an afternoon, not a quarter",
name: "Dana Whitfield",
role: "Director of Operations, healthcare startup",
initials: "DW",
avatar: "from-rose-500 to-amber-500",
},
];
function renderQuote(quote: string, highlight: string): ReactNode {
const idx = quote.indexOf(highlight);
if (idx === -1) return quote;
return (
<>
{quote.slice(0, idx)}
<span className="relative whitespace-nowrap font-semibold text-indigo-600 dark:text-indigo-300">
<span className="relative z-10">{highlight}</span>
<span
aria-hidden="true"
className="absolute inset-x-0 bottom-1 z-0 h-3 -skew-x-6 rounded-sm bg-indigo-200/60 dark:bg-indigo-400/20"
/>
</span>
{quote.slice(idx + highlight.length)}
</>
);
}
function Stars(): ReactNode {
return (
<div
role="img"
aria-label="Rated 5 out of 5 stars"
className="flex items-center justify-center gap-1"
>
{[0, 1, 2, 3, 4].map((i) => (
<svg
key={i}
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
className="h-5 w-5 text-amber-400 drop-shadow-sm"
>
<path d="M9.05 2.93c.3-.92 1.6-.92 1.9 0l1.36 4.18a1 1 0 0 0 .95.69h4.4c.97 0 1.37 1.24.59 1.81l-3.56 2.59a1 1 0 0 0-.36 1.12l1.36 4.18c.3.92-.76 1.69-1.54 1.12l-3.56-2.58a1 1 0 0 0-1.18 0l-3.56 2.58c-.78.57-1.84-.2-1.54-1.12l1.36-4.18a1 1 0 0 0-.36-1.12L1.4 9.6c-.78-.57-.38-1.81.59-1.81h4.4a1 1 0 0 0 .95-.69L9.05 2.93Z" />
</svg>
))}
</div>
);
}
export default function TstxSingleLarge() {
const [active, setActive] = useState(0);
const reduce = useReducedMotion();
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
const current = TESTIMONIALS[active];
function onKeyDown(event: KeyboardEvent<HTMLButtonElement>, index: number) {
const count = TESTIMONIALS.length;
let next = index;
if (event.key === "ArrowRight" || event.key === "ArrowDown") {
next = (index + 1) % count;
} else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
next = (index - 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();
}
return (
<section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 to-white px-6 py-24 sm:py-32 dark:from-slate-950 dark:to-slate-900">
<style>{`
@keyframes tstxsl-float-a {
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
50% { transform: translate3d(24px, -28px, 0) scale(1.08); }
}
@keyframes tstxsl-float-b {
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
50% { transform: translate3d(-30px, 20px, 0) scale(1.12); }
}
@keyframes tstxsl-shimmer {
0% { background-position: 0% 50%; }
100% { background-position: 200% 50%; }
}
.tstxsl-orb-a { animation: tstxsl-float-a 16s ease-in-out infinite; }
.tstxsl-orb-b { animation: tstxsl-float-b 21s ease-in-out infinite; }
.tstxsl-mark {
background-image: linear-gradient(100deg, #6366f1, #a855f7, #38bdf8, #6366f1);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
animation: tstxsl-shimmer 9s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.tstxsl-orb-a, .tstxsl-orb-b, .tstxsl-mark { animation: none !important; }
}
`}</style>
{/* Atmospheric background */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
<div className="tstxsl-orb-a absolute -left-24 top-4 h-80 w-80 rounded-full bg-indigo-400/25 blur-3xl dark:bg-indigo-500/20" />
<div className="tstxsl-orb-b absolute -right-20 bottom-0 h-96 w-96 rounded-full bg-violet-400/20 blur-3xl dark:bg-violet-600/20" />
<div
className="absolute inset-0 opacity-[0.4] dark:opacity-[0.25]"
style={{
backgroundImage:
"radial-gradient(circle at 1px 1px, rgb(100 116 139 / 0.18) 1px, transparent 0)",
backgroundSize: "28px 28px",
maskImage:
"radial-gradient(ellipse 70% 60% at 50% 40%, black, transparent 75%)",
WebkitMaskImage:
"radial-gradient(ellipse 70% 60% at 50% 40%, black, transparent 75%)",
}}
/>
</div>
<div className="relative mx-auto flex max-w-3xl flex-col items-center text-center">
{/* Eyebrow */}
<span className="inline-flex items-center gap-2 rounded-full border border-slate-200/80 bg-white/70 px-4 py-1.5 text-xs font-semibold uppercase tracking-[0.18em] text-slate-600 shadow-sm backdrop-blur dark:border-slate-700/70 dark:bg-slate-800/60 dark:text-slate-300">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4 text-indigo-500 dark:text-indigo-400">
<path
d="M12 3l1.9 4.6L18.5 9l-4.6 1.4L12 15l-1.9-4.6L5.5 9l4.6-1.4L12 3z"
fill="currentColor"
/>
</svg>
Customer story
</span>
{/* Giant decorative quote mark */}
<span
aria-hidden="true"
className="tstxsl-mark pointer-events-none mt-8 select-none font-serif text-[7rem] leading-[0.6] sm:text-[9rem]"
>
“
</span>
{/* Quote panel */}
<div className="mt-2 w-full" role="tabpanel" id="tstxsl-panel" aria-live="polite">
<AnimatePresence mode="wait">
<motion.figure
key={current.id}
initial={reduce ? { opacity: 1 } : { opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 1 } : { opacity: 0, y: -18 }}
transition={{ duration: reduce ? 0 : 0.5, ease: "easeOut" }}
className="flex flex-col items-center"
>
<blockquote className="text-balance text-2xl font-medium leading-snug tracking-tight text-slate-800 sm:text-3xl lg:text-4xl dark:text-slate-100">
{renderQuote(current.quote, current.highlight)}
</blockquote>
<div className="mt-8">
<Stars />
</div>
<figcaption className="mt-6 flex items-center gap-4">
<span
className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-gradient-to-br ${current.avatar} text-sm font-bold text-white shadow-lg ring-2 ring-white/70 dark:ring-slate-900/70`}
aria-hidden="true"
>
{current.initials}
</span>
<span className="text-left">
<span className="block text-base font-semibold text-slate-900 dark:text-white">
{current.name}
</span>
<span className="block text-sm text-slate-500 dark:text-slate-400">
{current.role}
</span>
</span>
</figcaption>
</motion.figure>
</AnimatePresence>
</div>
{/* Pager / tablist */}
<div
role="tablist"
aria-label="Featured customer testimonials"
className="mt-12 flex items-center gap-3"
>
{TESTIMONIALS.map((t, i) => {
const selected = i === active;
return (
<button
key={t.id}
ref={(el) => {
tabRefs.current[i] = el;
}}
type="button"
role="tab"
id={`tstxsl-tab-${t.id}`}
aria-selected={selected}
aria-controls="tstxsl-panel"
aria-label={`Show testimonial from ${t.name}`}
tabIndex={selected ? 0 : -1}
onClick={() => setActive(i)}
onKeyDown={(e) => onKeyDown(e, i)}
className={`group relative flex h-11 w-11 items-center justify-center rounded-full outline-none transition focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${
selected ? "scale-105" : "opacity-55 hover:opacity-100"
}`}
>
<span
className={`flex h-full w-full items-center justify-center rounded-full bg-gradient-to-br ${t.avatar} text-xs font-bold text-white shadow-md transition ${
selected
? "ring-2 ring-indigo-500 ring-offset-2 ring-offset-white dark:ring-offset-slate-900"
: ""
}`}
>
{t.initials}
</span>
</button>
);
})}
</div>
{/* Trust line */}
<p className="mt-8 text-sm text-slate-500 dark:text-slate-400">
<span className="font-semibold text-slate-700 dark:text-slate-200">4.9 / 5</span>{" "}
average rating across{" "}
<span className="font-semibold text-slate-700 dark:text-slate-200">1,200+</span>{" "}
teams running on Beacon
</p>
</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

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

Logos Quote Testimonial
Originalquote with a logo strip

