Web InnoventixFreeCode

Cards Avatar Testimonial

Original · free

testimonial cards with avatars

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

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

type Role = "Founders" | "Engineers" | "Marketers" | "Designers";
type Filter = "All" | Role;

interface Testimonial {
  id: string;
  name: string;
  title: string;
  company: string;
  role: Role;
  quote: string;
  rating: number;
  initials: string;
  gradient: string;
}

const TESTIMONIALS: Testimonial[] = [
  {
    id: "maya-okonkwo",
    name: "Maya Okonkwo",
    title: "Co-founder",
    company: "Bootstrapped SaaS",
    role: "Founders",
    rating: 5,
    initials: "MO",
    gradient: "from-indigo-500 to-violet-500",
    quote:
      "Cobalt replaced four spreadsheets and two freelancers we were paying to reconcile them. Our Monday revenue review dropped from a two-hour meeting to a twelve-minute stand-up, and the board finally stopped questioning my numbers.",
  },
  {
    id: "daniel-reyes",
    name: "Daniel Reyes",
    title: "Chief Executive",
    company: "Series A marketplace",
    role: "Founders",
    rating: 5,
    initials: "DR",
    gradient: "from-emerald-500 to-sky-500",
    quote:
      "I evaluated six tools over a weekend and Cobalt was the only one my whole team had adopted by Wednesday without a single training session. Onboarding that fast is unheard of for us.",
  },
  {
    id: "priya-nair",
    name: "Priya Nair",
    title: "Staff Engineer",
    company: "Enterprise platform team",
    role: "Engineers",
    rating: 5,
    initials: "PN",
    gradient: "from-sky-500 to-indigo-500",
    quote:
      "The API is honest about its limits and the webhooks actually fire on time. We wired Cobalt into our deploy pipeline in an afternoon and it hasn't paged us once in seven months.",
  },
  {
    id: "tomas-halvorsen",
    name: "Tomás Halvorsen",
    title: "Backend Lead",
    company: "Logistics startup",
    role: "Engineers",
    rating: 4,
    initials: "TH",
    gradient: "from-violet-500 to-rose-500",
    quote:
      "Query views that used to time out on our warehouse now return in under a second. I still want finer-grained role permissions, but Cobalt's performance under real load won me over.",
  },
  {
    id: "aisha-bello",
    name: "Aisha Bello",
    title: "Head of Growth",
    company: "Direct-to-consumer brand",
    role: "Marketers",
    rating: 5,
    initials: "AB",
    gradient: "from-rose-500 to-amber-500",
    quote:
      "I can finally answer which channel actually drove that revenue without exporting anything. Cobalt cut our monthly reporting from three days to one morning, and the attribution just makes sense.",
  },
  {
    id: "wen-li",
    name: "Wen Li",
    title: "Marketing Manager",
    company: "B2B agency",
    role: "Marketers",
    rating: 5,
    initials: "WL",
    gradient: "from-amber-500 to-emerald-500",
    quote:
      "We manage eleven client dashboards in Cobalt and every one refreshes on its own. What used to eat my Fridays is now a link I share on Thursday afternoon.",
  },
  {
    id: "sofia-marchetti",
    name: "Sofia Marchetti",
    title: "Product Designer",
    company: "Fintech app",
    role: "Designers",
    rating: 5,
    initials: "SM",
    gradient: "from-indigo-500 to-sky-500",
    quote:
      "Handoff stopped being a guessing game. Cobalt keeps the spec, the data, and the comments in one place, so engineering builds what we actually designed the first time.",
  },
  {
    id: "kofi-mensah",
    name: "Kofi Mensah",
    title: "Design Lead",
    company: "Remote studio",
    role: "Designers",
    rating: 4,
    initials: "KM",
    gradient: "from-violet-500 to-indigo-500",
    quote:
      "Our design system lives in Cobalt now and the whole distributed team stays in sync across five time zones. I'd love richer version history, but it's already the source of truth we never had.",
  },
];

const FILTERS: Filter[] = ["All", "Founders", "Engineers", "Marketers", "Designers"];

function StarRating({ 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-600"
          }
        >
          <path d="M10 1.6l2.6 5.27 5.82.85-4.21 4.1.99 5.79L10 14.87l-5.2 2.73.99-5.79-4.21-4.1 5.82-.85L10 1.6z" />
        </svg>
      ))}
    </div>
  );
}

function QuoteGlyph() {
  return (
    <svg viewBox="0 0 40 40" aria-hidden="true" className="h-8 w-8">
      <path
        d="M9 22c0-6 3.4-10.4 9-12.6l1.4 2.9c-3.3 1.4-5.1 3.4-5.6 5.9.5-.1.9-.1 1.4-.1 3 0 5.3 2.1 5.3 5.1S17.9 28.5 14.7 28.5 9 25.9 9 22zm18 0c0-6 3.4-10.4 9-12.6l1.4 2.9c-3.3 1.4-5.1 3.4-5.6 5.9.5-.1.9-.1 1.4-.1 3 0 5.3 2.1 5.3 5.1S35.9 28.5 32.7 28.5 27 25.9 27 22z"
        className="fill-indigo-200 dark:fill-indigo-500/30"
      />
    </svg>
  );
}

function Avatar({ initials, gradient }: { initials: string; gradient: string }) {
  return (
    <div
      aria-hidden="true"
      className={`grid h-11 w-11 shrink-0 place-items-center rounded-full bg-gradient-to-br ${gradient} text-sm font-semibold text-white shadow-sm ring-2 ring-white dark:ring-slate-900`}
    >
      {initials}
    </div>
  );
}

function Card({ t }: { t: Testimonial }) {
  return (
    <article className="group relative flex h-full flex-col rounded-2xl border border-slate-200 bg-white/85 p-6 shadow-sm backdrop-blur transition-all duration-300 hover:-translate-y-1 hover:border-indigo-300 hover:shadow-xl hover:shadow-indigo-500/10 dark:border-slate-800 dark:bg-slate-900/70 dark:hover:border-indigo-500/50">
      <div className="mb-4 flex items-start justify-between">
        <QuoteGlyph />
        <span className="rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 text-[11px] font-medium tracking-wide text-slate-500 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400">
          {t.role}
        </span>
      </div>

      <StarRating rating={t.rating} />

      <blockquote className="mt-4 flex-1 text-[15px] leading-relaxed text-slate-700 dark:text-slate-200">
        “{t.quote}”
      </blockquote>

      <div className="mt-6 flex items-center gap-3 border-t border-slate-100 pt-5 dark:border-slate-800">
        <Avatar initials={t.initials} gradient={t.gradient} />
        <div className="min-w-0">
          <p className="truncate text-sm font-semibold text-slate-900 dark:text-white">
            {t.name}
          </p>
          <p className="truncate text-xs text-slate-500 dark:text-slate-400">
            {t.title} · {t.company}
          </p>
        </div>
      </div>
    </article>
  );
}

export default function TstxCardsAvatar() {
  const reduce = useReducedMotion();
  const [active, setActive] = useState<Filter>("All");
  const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);

  const visible =
    active === "All" ? TESTIMONIALS : TESTIMONIALS.filter((t) => t.role === active);

  const avg = (
    TESTIMONIALS.reduce((sum, t) => sum + t.rating, 0) / TESTIMONIALS.length
  ).toFixed(1);

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-4 py-24 sm:py-32 dark:bg-slate-950">
      <style>{`
        @keyframes tstxca-drift {
          0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
          50% { transform: translate3d(0, -24px, 0) scale(1.08); }
        }
        .tstxca-blob { animation: tstxca-drift 16s ease-in-out infinite; }
        .tstxca-blob-slow { animation: tstxca-drift 22s ease-in-out infinite reverse; }
        @media (prefers-reduced-motion: reduce) {
          .tstxca-blob, .tstxca-blob-slow { animation: none !important; }
        }
      `}</style>

      {/* Decorative background */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
        <div className="tstxca-blob absolute -left-24 top-10 h-72 w-72 rounded-full bg-indigo-300/30 blur-3xl dark:bg-indigo-600/20" />
        <div className="tstxca-blob-slow absolute -right-20 bottom-0 h-80 w-80 rounded-full bg-violet-300/30 blur-3xl dark:bg-violet-600/20" />
        <div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-slate-300 to-transparent dark:via-slate-700" />
      </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-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-medium text-indigo-700 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300">
            <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
            Loved by working teams
          </span>

          <h2 className="mt-5 text-balance text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
            The people shipping with Cobalt say it best
          </h2>

          <p className="mt-4 text-balance text-base leading-relaxed text-slate-600 dark:text-slate-400">
            Real reviews from founders, engineers, marketers, and designers who moved
            their day-to-day work into one shared workspace.
          </p>

          <div className="mt-6 flex items-center justify-center gap-3">
            <StarRating rating={5} />
            <span className="text-sm font-medium text-slate-700 dark:text-slate-300">
              {avg} average from 2,600+ product teams
            </span>
          </div>
        </div>

        {/* Filter tabs */}
        <div
          role="tablist"
          aria-label="Filter testimonials by role"
          className="mx-auto mt-10 flex max-w-full flex-wrap items-center justify-center gap-2"
        >
          {FILTERS.map((f, i) => {
            const selected = active === f;
            return (
              <button
                key={f}
                ref={(el) => {
                  tabRefs.current[i] = el;
                }}
                role="tab"
                id={`tstxca-tab-${f}`}
                aria-selected={selected}
                aria-controls="tstxca-panel"
                tabIndex={selected ? 0 : -1}
                onClick={() => setActive(f)}
                onKeyDown={(e) => {
                  if (
                    e.key === "ArrowRight" ||
                    e.key === "ArrowLeft" ||
                    e.key === "Home" ||
                    e.key === "End"
                  ) {
                    e.preventDefault();
                    const last = FILTERS.length - 1;
                    let next = i;
                    if (e.key === "ArrowRight") next = i === last ? 0 : i + 1;
                    else if (e.key === "ArrowLeft") next = i === 0 ? last : i - 1;
                    else if (e.key === "Home") next = 0;
                    else next = last;
                    setActive(FILTERS[next]);
                    tabRefs.current[next]?.focus();
                  }
                }}
                className={
                  "rounded-full px-4 py-2 text-sm font-medium outline-none transition-colors 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 " +
                  (selected
                    ? "bg-slate-900 text-white shadow 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 dark:text-slate-400 dark:hover:text-white")
                }
              >
                {f}
              </button>
            );
          })}
        </div>

        <p aria-live="polite" className="sr-only">
          Showing {visible.length} {visible.length === 1 ? "review" : "reviews"} for {active}.
        </p>

        {/* Cards */}
        <motion.ul
          id="tstxca-panel"
          role="tabpanel"
          aria-labelledby={`tstxca-tab-${active}`}
          tabIndex={0}
          layout={reduce ? false : true}
          className="mt-12 grid list-none grid-cols-1 gap-6 outline-none sm:grid-cols-2 lg:grid-cols-3"
        >
          <AnimatePresence mode="popLayout" initial={false}>
            {visible.map((t) => (
              <motion.li
                key={t.id}
                layout={reduce ? false : true}
                initial={reduce ? false : { opacity: 0, y: 18, scale: 0.98 }}
                animate={reduce ? {} : { opacity: 1, y: 0, scale: 1 }}
                exit={reduce ? {} : { opacity: 0, scale: 0.96 }}
                transition={{ duration: 0.35, ease: "easeOut" }}
              >
                <Card t={t} />
              </motion.li>
            ))}
          </AnimatePresence>
        </motion.ul>
      </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

Logos Quote Testimonial

Logos Quote Testimonial

Original

quote with a logo strip