Web InnoventixFreeCode

Masonry Testimonial

Original · free

masonry testimonial wall

byWeb InnoventixReact + Tailwind
tstxmasonrytestimonials
Open

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
tstx-masonry.tsx
"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

motion

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 quote

Similar components

Browse all →
Three Card Testimonial Grid

Three Card Testimonial Grid

Original

A clean three-column grid of testimonial cards, each with a star rating, quote and an initial avatar with name and role.

Single Featured Quote

Single Featured Quote

Original

A 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

Logo Wall Marquee

Original

A pair of infinitely scrolling, pause-on-hover marquees pairing a customer logo wall with compact quote cards, with reduced-motion support.

Masonry Testimonial Wall

Masonry Testimonial Wall

Original

A 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

Aggregate Rating Spotlight

Original

A split layout pairing a large aggregate rating score, star row and overlapping avatar stack with two featured review cards.

Auto-Rotating Quote Carousel

Auto-Rotating Quote Carousel

Original

An 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

Staggered Reveal Testimonial Grid

Original

A 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

Infinite Marquee Testimonial Wall

Original

A 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

Grid Testimonial

Original

testimonial grid

Single Large Testimonial

Single Large Testimonial

Original

single large quote

Marquee Testimonial

Marquee Testimonial

Original

testimonial marquee rows

Cards Avatar Testimonial

Cards Avatar Testimonial

Original

testimonial cards with avatars