Web InnoventixFreeCode

Staggered Reveal Testimonial Grid

Original · free

A testimonial grid whose cards spring in with a staggered scroll reveal and light up with an animated gradient border on hover.

byWeb InnoventixReact + Tailwind
stagger animation gridframer motion testimonialsscroll reveal cardsanimated gradient borderwhileInView staggertestimonial grid react
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/atest-stagger-grid.json
atest-stagger-grid.tsx
"use client";

import { motion, useReducedMotion } from "motion/react";

const reviews = [
  {
    quote:
      "Six months in and I still open it every morning without a hint of dread. That alone puts it in a category of one for me.",
    name: "Helena Vance",
    role: "Design Lead, Everly",
    initials: "HV",
    tint: "from-emerald-500 to-teal-500",
    rating: 5,
    featured: true,
  },
  {
    quote: "Onboarding that used to take a fortnight now takes an afternoon.",
    name: "Marcus Bell",
    role: "Ops, Kestrel",
    initials: "MB",
    tint: "from-teal-500 to-cyan-500",
    rating: 5,
  },
  {
    quote: "The support team answers like people who actually use the product.",
    name: "Sofia Lindqvist",
    role: "PM, Loomly",
    initials: "SL",
    tint: "from-cyan-500 to-sky-500",
    rating: 5,
  },
  {
    quote: "It quietly does the boring work so we can do the interesting work.",
    name: "Tariq Hassan",
    role: "CTO, Brightfold",
    initials: "TH",
    tint: "from-green-500 to-emerald-500",
    rating: 5,
  },
  {
    quote: "Rock-solid uptime and a roadmap that clearly listens to us.",
    name: "Grace Okonkwo",
    role: "Founder, Palisade",
    initials: "GO",
    tint: "from-emerald-500 to-green-500",
    rating: 4,
  },
  {
    quote:
      "We compared five alternatives on a spreadsheet. This one won on every row that mattered, and on a few we did not expect to care about.",
    name: "Julian Ross",
    role: "Head of Growth, Meridian",
    initials: "JR",
    tint: "from-teal-500 to-emerald-500",
    rating: 5,
  },
];

export default function AtestStaggerGrid() {
  const reduce = useReducedMotion();

  const container = {
    hidden: {},
    show: { transition: { staggerChildren: 0.09, delayChildren: 0.05 } },
  };
  const item = reduce
    ? { hidden: { opacity: 0 }, show: { opacity: 1 } }
    : {
        hidden: { opacity: 0, y: 28, scale: 0.96 },
        show: {
          opacity: 1,
          y: 0,
          scale: 1,
          transition: { type: "spring" as const, stiffness: 260, damping: 24 },
        },
      };

  return (
    <section className="bg-white px-6 py-20 dark:bg-zinc-950 md:py-28">
      <style>{`
        @keyframes atest-sg-spin { to { transform: rotate(1turn); } }
        @keyframes atest-sg-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
        .atest-sg-conic {
          background: conic-gradient(from 0deg, transparent 0deg, #10b981 60deg, #06b6d4 140deg, #14b8a6 220deg, transparent 300deg);
          animation: atest-sg-spin 4.5s linear infinite;
        }
        .atest-sg-shimmer {
          background-image: linear-gradient(110deg, transparent 30%, rgba(16,185,129,0.12) 50%, transparent 70%);
          background-size: 200% 100%;
          animation: atest-sg-shimmer 3.5s linear infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .atest-sg-conic, .atest-sg-shimmer { animation: none; }
        }
      `}</style>

      <motion.div
        initial={reduce ? false : { opacity: 0, y: 16 }}
        whileInView={{ opacity: 1, y: 0 }}
        viewport={{ once: true, amount: 0.6 }}
        transition={{ duration: 0.5, ease: "easeOut" }}
        className="mx-auto max-w-2xl text-center"
      >
        <p className="text-sm font-semibold uppercase tracking-[0.2em] text-emerald-600 dark:text-emerald-400">
          Testimonials
        </p>
        <h2 className="mt-3 text-3xl font-bold tracking-tight text-balance text-zinc-900 sm:text-4xl dark:text-white">
          The proof turns up in the details
        </h2>
        <p className="mt-4 text-lg text-zinc-600 dark:text-zinc-400">
          Scroll a little and watch what teams say once the novelty has worn off.
        </p>
      </motion.div>

      <motion.ul
        variants={container}
        initial="hidden"
        whileInView="show"
        viewport={{ once: true, amount: 0.15 }}
        className="mx-auto mt-14 grid max-w-6xl auto-rows-fr grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3"
      >
        {reviews.map((r) => (
          <motion.li
            key={r.name}
            variants={item}
            whileHover={reduce ? undefined : { y: -6 }}
            transition={{ type: "spring", stiffness: 300, damping: 22 }}
            className={`group relative overflow-hidden rounded-2xl p-px ${
              r.featured ? "sm:col-span-2 lg:col-span-2" : ""
            }`}
          >
            {/* Animated gradient ring on hover */}
            <div
              aria-hidden="true"
              className="atest-sg-conic pointer-events-none absolute inset-[-120%] opacity-0 transition-opacity duration-500 group-hover:opacity-100"
            />

            <div className="relative flex h-full flex-col overflow-hidden rounded-[15px] border border-zinc-200 bg-zinc-50 p-6 dark:border-zinc-800 dark:bg-zinc-900">
              {r.featured && (
                <div
                  aria-hidden="true"
                  className="atest-sg-shimmer pointer-events-none absolute inset-0"
                />
              )}

              <div className="relative flex items-center justify-between">
                <div className="flex items-center gap-0.5" aria-label={`Rated ${r.rating} out of 5`}>
                  {[0, 1, 2, 3, 4].map((s) => (
                    <svg
                      key={s}
                      viewBox="0 0 24 24"
                      className={
                        s < r.rating ? "h-4 w-4 fill-amber-400" : "h-4 w-4 fill-zinc-300 dark:fill-zinc-700"
                      }
                      aria-hidden="true"
                    >
                      <path d="M11.48 3.5a.56.56 0 011.04 0l2.12 5.11a.56.56 0 00.48.35l5.52.44c.5.04.7.66.32.99l-4.2 3.6a.56.56 0 00-.19.56l1.29 5.38a.56.56 0 01-.84.61l-4.73-2.88a.56.56 0 00-.58 0l-4.73 2.88a.56.56 0 01-.84-.61l1.29-5.38a.56.56 0 00-.19-.56l-4.2-3.6a.56.56 0 01.32-.99l5.52-.44a.56.56 0 00.48-.35L11.48 3.5z" />
                    </svg>
                  ))}
                </div>
                <svg
                  aria-hidden="true"
                  viewBox="0 0 24 24"
                  className="h-6 w-6 fill-emerald-500/15 text-emerald-500/40"
                >
                  <path d="M9.5 6C6.5 7 5 9.5 5 13v5h6v-6H8c0-2 1-3.2 3-3.7L9.5 6zm9 0c-3 1-4.5 3.5-4.5 7v5h6v-6h-3c0-2 1-3.2 3-3.7L18.5 6z" />
                </svg>
              </div>

              <blockquote
                className={`relative mt-4 flex-1 text-pretty text-zinc-700 dark:text-zinc-300 ${
                  r.featured ? "text-lg font-medium sm:text-xl" : "text-[15px]"
                }`}
              >
                {r.quote}
              </blockquote>

              <figcaption className="relative mt-6 flex items-center gap-3">
                <span
                  className={`grid h-10 w-10 shrink-0 place-items-center rounded-full bg-gradient-to-br ${r.tint} text-xs font-semibold text-white shadow-md`}
                  aria-hidden="true"
                >
                  {r.initials}
                </span>
                <span className="min-w-0">
                  <span className="block truncate text-sm font-semibold text-zinc-900 dark:text-white">
                    {r.name}
                  </span>
                  <span className="block truncate text-xs text-zinc-500 dark:text-zinc-400">
                    {r.role}
                  </span>
                </span>
              </figcaption>
            </div>
          </motion.li>
        ))}
      </motion.ul>
    </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.

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.

Spotlight Hero

Spotlight Hero

Original

A centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero

Split Hero

Original

A two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero

Gradient Spotlight Hero

Original

A minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

App Preview Hero

App Preview Hero

Original

A centred hero that pairs headline copy with a realistic product dashboard mock built entirely from markup, complete with browser chrome and a floating notification card.

Waitlist Capture Hero

Waitlist Capture Hero

Original

A dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.