Masonry Testimonial
Original · freemasonry testimonial wall
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-masonry.json"use client";
import { useMemo, useState } from "react";
import { motion, useReducedMotion } from "motion/react";
type Category = "founders" | "engineering" | "design" | "marketing" | "support";
interface Testimonial {
id: number;
quote: string;
name: string;
role: string;
category: Category;
rating: number;
initials: string;
accent: string;
featured?: boolean;
}
interface Filter {
id: Category | "all";
label: string;
}
const FILTERS: Filter[] = [
{ id: "all", label: "Everyone" },
{ id: "founders", label: "Founders" },
{ id: "engineering", label: "Engineering" },
{ id: "design", label: "Design" },
{ id: "marketing", label: "Marketing" },
{ id: "support", label: "Support" },
];
const TESTIMONIALS: Testimonial[] = [
{
id: 1,
quote:
"We collapsed three separate tools into Northline in a single afternoon. Two quarters later our activation rate is up 34% and I can finally see exactly where new accounts stall — without pinging engineering for a query.",
name: "Priya Nandakumar",
role: "Co-founder & CEO, early-stage fintech",
category: "founders",
rating: 5,
initials: "PN",
accent: "from-indigo-500 to-violet-500",
featured: true,
},
{
id: 2,
quote:
"The event API is the first analytics SDK I haven't wanted to rip out after a month. Typed payloads, sane batching, and no mystery network calls in the profiler.",
name: "Marcus Feldt",
role: "Staff Engineer, B2B payments",
category: "engineering",
rating: 5,
initials: "MF",
accent: "from-sky-500 to-cyan-500",
},
{
id: 3,
quote:
"I shipped a full funnel report during our Monday standup and the whole team was reacting in Slack before I'd finished talking. That never happened with the old stack.",
name: "Dana Okonkwo",
role: "Head of Growth, D2C skincare",
category: "marketing",
rating: 5,
initials: "DO",
accent: "from-rose-500 to-pink-500",
},
{
id: 4,
quote:
"Dashboards that don't fight my design system. Every chart inherits our tokens, so the customer-facing reports look like they were hand-built by our team.",
name: "Yuki Tanaka",
role: "Design Lead, workflow SaaS",
category: "design",
rating: 4,
initials: "YT",
accent: "from-emerald-500 to-teal-500",
},
{
id: 5,
quote:
"When a customer writes in confused, I pull up their session timeline in Northline and know the answer before I reply. My median resolution time dropped from 9 hours to under 2.",
name: "Rafael Mendes",
role: "Support Team Lead, logistics platform",
category: "support",
rating: 5,
initials: "RM",
accent: "from-amber-500 to-orange-500",
},
{
id: 6,
quote:
"As a non-technical founder, I was terrified of another tool I'd need a data hire to run. I built our north-star dashboard myself on day one.",
name: "Sofia Alvarez",
role: "Founder, marketplace startup",
category: "founders",
rating: 5,
initials: "SA",
accent: "from-violet-500 to-fuchsia-500",
featured: true,
},
{
id: 7,
quote:
"Retention cohorts that actually load. We backfilled 18 months of history and the queries still came back in under two seconds.",
name: "Aisha Rahman",
role: "Principal Engineer, developer tools",
category: "engineering",
rating: 5,
initials: "AR",
accent: "from-sky-500 to-indigo-500",
},
{
id: 8,
quote:
"We finally killed the weekly spreadsheet ritual. Attribution, channel spend, and payback are all one saved view now — the exec team just opens the link.",
name: "Tom Becker",
role: "VP Marketing, B2B analytics",
category: "marketing",
rating: 4,
initials: "TB",
accent: "from-rose-500 to-red-500",
},
{
id: 9,
quote:
"The empty states, the loading skeletons, the keyboard shortcuts — someone clearly cared about the last 10%. It's a genuine pleasure to use daily.",
name: "Noah Kim",
role: "Product Designer, HR platform",
category: "design",
rating: 5,
initials: "NK",
accent: "from-emerald-500 to-green-500",
},
{
id: 10,
quote:
"Onboarding our support org took one 20-minute walkthrough. No SQL, no data team gatekeeping — every agent can answer their own questions now.",
name: "Elena Popova",
role: "Director of CX, travel tech",
category: "support",
rating: 5,
initials: "EP",
accent: "from-amber-500 to-yellow-500",
},
{
id: 11,
quote:
"Switching cost was my biggest fear and it turned out to be a weekend. The migration importer mapped our old events automatically and flagged the three that needed a human.",
name: "Grace Whitmore",
role: "Co-founder & CTO, climate SaaS",
category: "founders",
rating: 5,
initials: "GW",
accent: "from-indigo-500 to-blue-500",
},
{
id: 12,
quote:
"Self-serve, well-documented, and it respects my time. I've filed exactly zero support tickets in six months of production use.",
name: "Daniel Osei",
role: "Senior Engineer, edtech",
category: "engineering",
rating: 5,
initials: "DO",
accent: "from-cyan-500 to-sky-500",
},
];
function Stars({ rating }: { rating: number }) {
return (
<div
className="flex items-center gap-0.5"
role="img"
aria-label={`Rated ${rating} out of 5`}
>
{[0, 1, 2, 3, 4].map((i) => (
<svg
key={i}
viewBox="0 0 20 20"
className={
i < rating
? "h-4 w-4 fill-amber-400"
: "h-4 w-4 fill-slate-200 dark:fill-slate-700"
}
aria-hidden="true"
>
<path d="M10 1.5l2.47 5.01 5.53.8-4 3.9.94 5.5L10 14.9l-4.94 2.6.94-5.5-4-3.9 5.53-.8L10 1.5z" />
</svg>
))}
</div>
);
}
function QuoteMark() {
return (
<svg
viewBox="0 0 40 40"
className="h-7 w-7 fill-slate-200 dark:fill-slate-700"
aria-hidden="true"
>
<path d="M12 8C7 8 4 12 4 17c0 5 3 8 7 8 1 0 2-.2 2.6-.5C12 27 9.5 29 6 30l1.5 2c6-1.5 10-6.5 10-13 0-6.5-3-11-5.5-11zm18 0c-5 0-8 4-8 9 0 5 3 8 7 8 1 0 2-.2 2.6-.5C30 27 27.5 29 24 30l1.5 2c6-1.5 10-6.5 10-13 0-6.5-3-11-5.5-11z" />
</svg>
);
}
export default function TstxMasonry() {
const [active, setActive] = useState<Category | "all">("all");
const reduce = useReducedMotion();
const visible = useMemo(
() =>
active === "all"
? TESTIMONIALS
: TESTIMONIALS.filter((t) => t.category === active),
[active]
);
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-6 py-24 text-slate-900 sm:px-8 sm:py-32 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes tstxm-drift {
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
50% { transform: translate3d(0, -24px, 0) scale(1.06); }
}
@keyframes tstxm-drift-slow {
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
50% { transform: translate3d(18px, 14px, 0) scale(1.1); }
}
@keyframes tstxm-sheen {
0% { background-position: 0% 50%; }
100% { background-position: 200% 50%; }
}
.tstxm-blob-a { animation: tstxm-drift 16s ease-in-out infinite; }
.tstxm-blob-b { animation: tstxm-drift-slow 22s ease-in-out infinite; }
.tstxm-featured::before {
content: "";
position: absolute;
inset: -1px;
border-radius: 1rem;
padding: 1px;
background: linear-gradient(110deg, rgba(99,102,241,0.9), rgba(168,85,247,0.5), rgba(56,189,248,0.9), rgba(99,102,241,0.9));
background-size: 200% 100%;
animation: tstxm-sheen 6s linear infinite;
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.tstxm-blob-a, .tstxm-blob-b, .tstxm-featured::before {
animation: none !important;
}
}
`}</style>
{/* Atmospheric background */}
<div className="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true">
<div className="tstxm-blob-a absolute -top-24 -left-24 h-96 w-96 rounded-full bg-indigo-300/30 blur-3xl dark:bg-indigo-600/20" />
<div className="tstxm-blob-b absolute top-1/3 -right-24 h-96 w-96 rounded-full bg-violet-300/30 blur-3xl dark:bg-violet-600/20" />
<div
className="absolute inset-0 opacity-[0.035] dark:opacity-[0.05]"
style={{
backgroundImage:
"linear-gradient(to right, currentColor 1px, transparent 1px), linear-gradient(to bottom, currentColor 1px, transparent 1px)",
backgroundSize: "56px 56px",
}}
/>
</div>
<div className="relative mx-auto max-w-6xl">
{/* Header */}
<div 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-4 py-1.5 text-xs font-medium tracking-wide text-slate-600 backdrop-blur dark:border-slate-800 dark:bg-slate-900/60 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-400 opacity-60" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
</span>
2,000+ teams ship with Northline
</span>
<h2 className="mt-6 text-balance text-4xl font-semibold tracking-tight sm:text-5xl">
The wall of teams who
<span className="bg-gradient-to-r from-indigo-500 via-violet-500 to-sky-500 bg-clip-text text-transparent">
{" "}
stopped guessing
</span>
</h2>
<p className="mt-4 text-pretty text-base leading-relaxed text-slate-600 sm:text-lg dark:text-slate-400">
Real words from founders, engineers, and operators who replaced the
spreadsheet ritual with answers they trust. Filter by the role that
sounds most like yours.
</p>
{/* Aggregate stat row */}
<div className="mt-8 flex flex-wrap items-center justify-center gap-x-8 gap-y-3">
<div className="flex items-center gap-2">
<Stars rating={5} />
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">
4.9 average
</span>
</div>
<div className="h-4 w-px bg-slate-300 dark:bg-slate-700" />
<span className="text-sm text-slate-600 dark:text-slate-400">
<span className="font-semibold text-slate-900 dark:text-slate-100">
98%
</span>{" "}
would recommend
</span>
<div className="h-4 w-px bg-slate-300 dark:bg-slate-700" />
<span className="text-sm text-slate-600 dark:text-slate-400">
<span className="font-semibold text-slate-900 dark:text-slate-100">
412
</span>{" "}
verified reviews
</span>
</div>
</div>
{/* Filter controls */}
<div
className="mt-12 flex flex-wrap items-center justify-center gap-2"
role="group"
aria-label="Filter testimonials by role"
>
{FILTERS.map((f) => {
const isActive = active === f.id;
return (
<button
key={f.id}
type="button"
onClick={() => setActive(f.id)}
aria-pressed={isActive}
className={
"rounded-full px-4 py-2 text-sm font-medium transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:focus-visible:ring-offset-slate-950 " +
(isActive
? "bg-slate-900 text-white shadow-sm dark:bg-white dark:text-slate-900"
: "border border-slate-200 bg-white/70 text-slate-600 hover:border-slate-300 hover:text-slate-900 dark:border-slate-800 dark:bg-slate-900/60 dark:text-slate-400 dark:hover:border-slate-700 dark:hover:text-slate-100")
}
>
{f.label}
</button>
);
})}
</div>
{/* Masonry wall */}
<div
key={active}
role="list"
className="mt-12 gap-5 [column-fill:_balance] sm:columns-2 lg:columns-3"
>
{visible.map((t, i) => (
<motion.article
role="listitem"
key={t.id}
initial={reduce ? false : { opacity: 0, y: 18 }}
whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-40px" }}
transition={{
duration: 0.5,
delay: reduce ? 0 : Math.min(i * 0.06, 0.45),
ease: [0.22, 1, 0.36, 1],
}}
className={
"relative mb-5 break-inside-avoid rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-shadow duration-300 hover:shadow-lg dark:border-slate-800 dark:bg-slate-900 " +
(t.featured ? "tstxm-featured" : "")
}
>
<div className="flex items-start justify-between">
<Stars rating={t.rating} />
<QuoteMark />
</div>
<blockquote className="mt-4 text-[0.95rem] leading-relaxed text-slate-700 dark:text-slate-300">
{t.quote}
</blockquote>
<figcaption className="mt-6 flex items-center gap-3 border-t border-slate-100 pt-4 dark:border-slate-800">
<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 shadow-inner " +
t.accent
}
aria-hidden="true"
>
{t.initials}
</span>
<span className="min-w-0">
<span className="block truncate text-sm font-semibold text-slate-900 dark:text-slate-100">
{t.name}
</span>
<span className="block truncate text-xs text-slate-500 dark:text-slate-400">
{t.role}
</span>
</span>
</figcaption>
</motion.article>
))}
</div>
{/* CTA footer */}
<div className="mt-14 flex flex-col items-center gap-4 text-center sm:flex-row sm:justify-center">
<a
href="#"
className="inline-flex items-center gap-2 rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white shadow-sm transition-transform duration-200 hover:-translate-y-0.5 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:bg-white dark:text-slate-900 dark:focus-visible:ring-offset-slate-950"
>
Start free with Northline
<svg viewBox="0 0 20 20" className="h-4 w-4 fill-current" aria-hidden="true">
<path d="M7.5 4l6 6-6 6-1.4-1.4L10.7 10 6.1 5.4 7.5 4z" />
</svg>
</a>
<span className="text-sm text-slate-500 dark:text-slate-400">
No card required — read all 412 reviews inside.
</span>
</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

