Web InnoventixFreeCode

Grid Testimonial

Original · free

testimonial grid

byWeb InnoventixReact + Tailwind
tstxgridtestimonials
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-grid.json
tstx-grid.tsx
"use client";

import { useMemo, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type Team = "Engineering" | "Product" | "Design" | "Marketing" | "Operations";
type Filter = Team | "All";

type Testimonial = {
  id: string;
  quote: string;
  name: string;
  role: string;
  org: string;
  team: Team;
  rating: number;
  accent: string;
  featured?: boolean;
};

const TESTIMONIALS: Testimonial[] = [
  {
    id: "t1",
    quote:
      "Onboarding used to eat a full week per hire. With Northwind's shared workspaces, new engineers ship their first real change on day two. It paid for itself before the trial ran out.",
    name: "Priya Raman",
    role: "Engineering Lead",
    org: "logistics platform",
    team: "Engineering",
    rating: 5,
    accent: "from-indigo-500 to-violet-500",
    featured: true,
  },
  {
    id: "t2",
    quote:
      "I stopped dreading the Monday status meeting. Everything lives on one board, so I walk in already knowing where every project actually stands.",
    name: "Marcus Feld",
    role: "Product Manager",
    org: "B2B fintech",
    team: "Product",
    rating: 5,
    accent: "from-sky-500 to-indigo-500",
  },
  {
    id: "t3",
    quote:
      "The automated checks flagged a billing regression two hours before launch. That single catch saved us a very awkward all-customer email.",
    name: "Dana Osei",
    role: "QA Lead",
    org: "subscription commerce",
    team: "Engineering",
    rating: 5,
    accent: "from-emerald-500 to-sky-500",
  },
  {
    id: "t4",
    quote:
      "We folded four separate tools into Northwind and cut our software bill by 38%. Nobody on the team misses the constant tab-switching.",
    name: "Lena Vogt",
    role: "Operations Director",
    org: "healthcare SaaS",
    team: "Operations",
    rating: 5,
    accent: "from-amber-500 to-rose-500",
  },
  {
    id: "t5",
    quote:
      "As a designer I care about the small things. The keyboard shortcuts and the clean handoff view feel like they were built by people who actually ship.",
    name: "Noah Kim",
    role: "Senior Product Designer",
    org: "mobile studio",
    team: "Design",
    rating: 5,
    accent: "from-violet-500 to-rose-500",
  },
  {
    id: "t6",
    quote:
      "Our marketing calendar finally matches reality. Campaigns that used to slip by weeks now go out on the exact day we planned them.",
    name: "Elena Cruz",
    role: "Marketing Lead",
    org: "DTC brand",
    team: "Marketing",
    rating: 5,
    accent: "from-rose-500 to-amber-500",
  },
  {
    id: "t7",
    quote:
      "Migrations terrified me. Their team moved five years of records over a weekend, and Monday morning everything just worked.",
    name: "Aisha Bello",
    role: "IT Manager",
    org: "regional bank",
    team: "Engineering",
    rating: 4,
    accent: "from-sky-500 to-emerald-500",
  },
  {
    id: "t8",
    quote:
      "Tickets asking 'where is this document' basically disappeared. The team finally trusts one source of truth instead of six chat threads.",
    name: "Sam Whitfield",
    role: "Head of Support",
    org: "edtech",
    team: "Operations",
    rating: 5,
    accent: "from-indigo-500 to-emerald-500",
  },
  {
    id: "t9",
    quote:
      "Roadmap reviews went from a two-day spreadsheet ritual to a live board my whole leadership team reads on their own. That alone changed how we plan.",
    name: "Tomás Ferreira",
    role: "VP of Product",
    org: "workforce startup",
    team: "Product",
    rating: 5,
    accent: "from-violet-500 to-indigo-500",
  },
];

const FILTERS: Filter[] = [
  "All",
  "Engineering",
  "Product",
  "Design",
  "Marketing",
  "Operations",
];

const STATS: { value: string; label: string }[] = [
  { value: "4.9/5", label: "average rating" },
  { value: "1,200+", label: "verified teams" },
  { value: "38%", label: "median tool spend saved" },
  { value: "2 days", label: "to first shipped change" },
];

function initials(name: string): string {
  return name
    .split(" ")
    .map((part) => part.charAt(0))
    .slice(0, 2)
    .join("")
    .toUpperCase();
}

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"
          aria-hidden="true"
          className={
            i < rating
              ? "h-4 w-4 fill-amber-400"
              : "h-4 w-4 fill-slate-300 dark:fill-slate-700"
          }
        >
          <path d="M10 1.6l2.47 5.01 5.53.8-4 3.9.94 5.5L10 14.2l-4.94 2.6.94-5.5-4-3.9 5.53-.8z" />
        </svg>
      ))}
    </div>
  );
}

export default function TstxGrid() {
  const reduce = useReducedMotion();
  const [active, setActive] = useState<Filter>("All");

  const visible = useMemo(
    () =>
      active === "All"
        ? TESTIMONIALS
        : TESTIMONIALS.filter((t) => t.team === active),
    [active],
  );

  return (
    <section className="relative w-full overflow-hidden bg-white px-5 py-24 text-slate-900 sm:px-8 sm:py-28 dark:bg-slate-950 dark:text-white">
      <style>{`
        @keyframes tstxgrid-float {
          0%, 100% { transform: translate3d(0, 0, 0); }
          50% { transform: translate3d(0, -26px, 0); }
        }
        @keyframes tstxgrid-float-alt {
          0%, 100% { transform: translate3d(0, 0, 0); }
          50% { transform: translate3d(0, 22px, 0); }
        }
        .tstxgrid-orb-a { animation: tstxgrid-float 13s ease-in-out infinite; }
        .tstxgrid-orb-b { animation: tstxgrid-float-alt 16s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .tstxgrid-orb-a, .tstxgrid-orb-b { animation: none !important; }
        }
      `}</style>

      {/* Decorative background */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 overflow-hidden"
      >
        <div className="tstxgrid-orb-a absolute -left-24 top-10 h-72 w-72 rounded-full bg-indigo-400/25 blur-3xl dark:bg-indigo-600/20" />
        <div className="tstxgrid-orb-b absolute -right-16 top-1/3 h-80 w-80 rounded-full bg-violet-400/20 blur-3xl dark:bg-violet-600/20" />
        <div className="tstxgrid-orb-a absolute bottom-0 left-1/3 h-64 w-64 rounded-full bg-sky-300/20 blur-3xl dark:bg-sky-700/15" />
      </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-3.5 py-1.5 text-xs font-semibold uppercase tracking-wider text-indigo-600 backdrop-blur dark:border-slate-800 dark:bg-slate-900/70 dark:text-indigo-300">
            <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
            Customer stories
          </span>
          <h2 className="mt-5 text-balance text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl">
            Teams don&apos;t just try Northwind.{" "}
            <span className="bg-gradient-to-r from-indigo-500 to-violet-500 bg-clip-text text-transparent">
              They keep it.
            </span>
          </h2>
          <p className="mt-5 text-pretty text-base leading-relaxed text-slate-600 sm:text-lg dark:text-slate-400">
            Over 1,200 product, engineering, and design teams run their day-to-day
            on Northwind. Here is what a few of them told us, unedited.
          </p>
        </div>

        {/* Stats */}
        <dl className="mx-auto mt-12 grid max-w-3xl grid-cols-2 gap-px overflow-hidden rounded-2xl border border-slate-200 bg-slate-200 sm:grid-cols-4 dark:border-slate-800 dark:bg-slate-800">
          {STATS.map((s) => (
            <div
              key={s.label}
              className="bg-white px-4 py-5 text-center dark:bg-slate-950"
            >
              <dt className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white">
                {s.value}
              </dt>
              <dd className="mt-1 text-xs font-medium text-slate-500 dark:text-slate-400">
                {s.label}
              </dd>
            </div>
          ))}
        </dl>

        {/* Filters */}
        <div
          className="mt-12 flex flex-wrap items-center justify-center gap-2"
          role="group"
          aria-label="Filter testimonials by team"
        >
          {FILTERS.map((f) => {
            const isActive = f === active;
            return (
              <button
                key={f}
                type="button"
                aria-pressed={isActive}
                onClick={() => setActive(f)}
                className={
                  "rounded-full px-4 py-2 text-sm font-semibold transition-colors focus:outline-none 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-950 " +
                  (isActive
                    ? "bg-slate-900 text-white shadow-sm dark:bg-white dark:text-slate-900"
                    : "border border-slate-200 bg-white 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-white")
                }
              >
                {f}
              </button>
            );
          })}
        </div>

        {/* Grid */}
        <motion.div
          layout={!reduce}
          className="mt-10 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3"
        >
          <AnimatePresence mode="popLayout" initial={false}>
            {visible.map((t, i) => (
              <motion.article
                key={t.id}
                layout={!reduce}
                initial={reduce ? { opacity: 0 } : { opacity: 0, y: 24, scale: 0.98 }}
                animate={{ opacity: 1, y: 0, scale: 1 }}
                exit={reduce ? { opacity: 0 } : { opacity: 0, y: -12, scale: 0.98 }}
                transition={{
                  duration: reduce ? 0 : 0.42,
                  delay: reduce ? 0 : Math.min(i * 0.04, 0.28),
                  ease: [0.22, 1, 0.36, 1],
                }}
                className={
                  "group relative flex flex-col rounded-2xl border p-6 transition-colors duration-300 focus-within:ring-2 focus-within:ring-indigo-500 hover:-translate-y-1 " +
                  (t.featured
                    ? "border-transparent bg-gradient-to-br from-indigo-50 to-violet-50 sm:col-span-2 lg:col-span-2 dark:from-indigo-950/40 dark:to-violet-950/30"
                    : "border-slate-200 bg-white hover:border-slate-300 dark:border-slate-800 dark:bg-slate-900/60 dark:hover:border-slate-700")
                }
              >
                {t.featured ? (
                  <span
                    aria-hidden="true"
                    className="pointer-events-none absolute inset-0 rounded-2xl ring-1 ring-inset ring-indigo-300/60 dark:ring-indigo-500/30"
                  />
                ) : null}

                <div className="flex items-center justify-between">
                  <svg
                    viewBox="0 0 24 24"
                    aria-hidden="true"
                    className="h-8 w-8 fill-indigo-400/70 dark:fill-indigo-500/60"
                  >
                    <path d="M9.6 5.2C6.5 6.7 4.6 9.6 4.6 13.1v5.7h6.1v-6.1H7.9c0-2.1 1-3.7 3-4.6l-1.3-2.9zm9.8 0c-3.1 1.5-5 4.4-5 7.9v5.7h6.1v-6.1h-2.8c0-2.1 1-3.7 3-4.6l-1.3-2.9z" />
                  </svg>
                  {t.featured ? (
                    <span className="rounded-full bg-white/70 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wide text-indigo-600 dark:bg-slate-900/60 dark:text-indigo-300">
                      Editor&apos;s pick
                    </span>
                  ) : (
                    <Stars rating={t.rating} />
                  )}
                </div>

                <blockquote
                  className={
                    "mt-4 flex-1 text-pretty leading-relaxed text-slate-700 dark:text-slate-200 " +
                    (t.featured ? "text-lg sm:text-xl" : "text-base")
                  }
                >
                  &ldquo;{t.quote}&rdquo;
                </blockquote>

                {t.featured ? (
                  <div className="mt-4">
                    <Stars rating={t.rating} />
                  </div>
                ) : null}

                <figcaption className="mt-6 flex items-center gap-3">
                  <span
                    aria-hidden="true"
                    className={
                      "flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-gradient-to-br text-sm font-bold text-white shadow-sm " +
                      t.accent
                    }
                  >
                    {initials(t.name)}
                  </span>
                  <span className="min-w-0">
                    <span className="block truncate text-sm font-semibold text-slate-900 dark:text-white">
                      {t.name}
                    </span>
                    <span className="block truncate text-sm text-slate-500 dark:text-slate-400">
                      {t.role}, {t.org}
                    </span>
                  </span>
                </figcaption>
              </motion.article>
            ))}
          </AnimatePresence>
        </motion.div>

        {/* Empty state guard (never triggers with current data, but safe) */}
        {visible.length === 0 ? (
          <p className="mt-10 text-center text-sm text-slate-500 dark:text-slate-400">
            No stories from this team yet.
          </p>
        ) : null}

        {/* Footer CTA */}
        <div className="mt-14 flex flex-col items-center justify-center gap-4 text-center sm:flex-row">
          <a
            href="#"
            className="inline-flex items-center gap-2 rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-slate-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200 dark:focus-visible:ring-offset-slate-950"
          >
            Read all 1,200 stories
            <svg
              viewBox="0 0 20 20"
              aria-hidden="true"
              className="h-4 w-4 fill-current"
            >
              <path d="M11.3 4.3a1 1 0 011.4 0l5 5a1 1 0 010 1.4l-5 5a1 1 0 01-1.4-1.4L14.6 11H4a1 1 0 110-2h10.6l-3.3-3.3a1 1 0 010-1.4z" />
            </svg>
          </a>
          <span className="text-sm text-slate-500 dark:text-slate-400">
            Every review is from a verified, paying Northwind team.
          </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.

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

Logos Quote Testimonial

Logos Quote Testimonial

Original

quote with a logo strip