Web InnoventixFreeCode

Logos Quote Testimonial

Original · free

quote with a logo strip

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

import { useId, useRef, useState, type KeyboardEvent, type ReactNode } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

const ACCENTS = {
  indigo: {
    text: "text-indigo-600 dark:text-indigo-400",
    ring: "ring-indigo-400/60 dark:ring-indigo-500/50",
    chip: "bg-indigo-50 dark:bg-indigo-500/10",
    avatar: "from-indigo-500 to-violet-500",
    glow: "from-indigo-400/40 to-violet-400/20 dark:from-indigo-500/25 dark:to-violet-500/10",
  },
  emerald: {
    text: "text-emerald-600 dark:text-emerald-400",
    ring: "ring-emerald-400/60 dark:ring-emerald-500/50",
    chip: "bg-emerald-50 dark:bg-emerald-500/10",
    avatar: "from-emerald-500 to-sky-500",
    glow: "from-emerald-400/40 to-sky-400/20 dark:from-emerald-500/25 dark:to-sky-500/10",
  },
  rose: {
    text: "text-rose-600 dark:text-rose-400",
    ring: "ring-rose-400/60 dark:ring-rose-500/50",
    chip: "bg-rose-50 dark:bg-rose-500/10",
    avatar: "from-rose-500 to-amber-500",
    glow: "from-rose-400/40 to-amber-400/20 dark:from-rose-500/25 dark:to-amber-500/10",
  },
  amber: {
    text: "text-amber-600 dark:text-amber-400",
    ring: "ring-amber-400/60 dark:ring-amber-500/50",
    chip: "bg-amber-50 dark:bg-amber-500/10",
    avatar: "from-amber-500 to-rose-500",
    glow: "from-amber-400/40 to-rose-400/20 dark:from-amber-500/25 dark:to-rose-500/10",
  },
  sky: {
    text: "text-sky-600 dark:text-sky-400",
    ring: "ring-sky-400/60 dark:ring-sky-500/50",
    chip: "bg-sky-50 dark:bg-sky-500/10",
    avatar: "from-sky-500 to-indigo-500",
    glow: "from-sky-400/40 to-indigo-400/20 dark:from-sky-500/25 dark:to-indigo-500/10",
  },
  violet: {
    text: "text-violet-600 dark:text-violet-400",
    ring: "ring-violet-400/60 dark:ring-violet-500/50",
    chip: "bg-violet-50 dark:bg-violet-500/10",
    avatar: "from-violet-500 to-indigo-500",
    glow: "from-violet-400/40 to-indigo-400/20 dark:from-violet-500/25 dark:to-indigo-500/10",
  },
} as const;

type AccentKey = keyof typeof ACCENTS;

type Testimonial = {
  id: string;
  company: string;
  accent: AccentKey;
  mark: ReactNode;
  quote: string;
  author: string;
  role: string;
  metricValue: string;
  metricLabel: string;
};

const TESTIMONIALS: Testimonial[] = [
  {
    id: "vertex",
    company: "Vertex Labs",
    accent: "indigo",
    mark: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinejoin="round" className="h-full w-full" aria-hidden="true">
        <path d="M12 3 21 20H3z" />
        <path d="M12 9 16.5 18h-9z" fill="currentColor" stroke="none" opacity="0.35" />
      </svg>
    ),
    quote:
      "We cut our release cycle from three weeks to four days. Cadence gave every engineer one place to see what's shipping and why it actually matters to the roadmap.",
    author: "Priya Nandakumar",
    role: "VP of Engineering",
    metricValue: "5x",
    metricLabel: "Faster releases",
  },
  {
    id: "halo",
    company: "Halo Health",
    accent: "emerald",
    mark: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" className="h-full w-full" aria-hidden="true">
        <circle cx="12" cy="12" r="8.5" />
        <path d="M12 8v8M8 12h8" />
      </svg>
    ),
    quote:
      "Onboarding a new clinic used to take a full quarter of back-and-forth. Now it takes an afternoon, and nothing quietly falls through the cracks on the way.",
    author: "Marcus Feld",
    role: "Director of Operations",
    metricValue: "92%",
    metricLabel: "Less setup time",
  },
  {
    id: "northpeak",
    company: "Northpeak",
    accent: "rose",
    mark: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinejoin="round" strokeLinecap="round" className="h-full w-full" aria-hidden="true">
        <path d="M2 19 8.5 7l4 6 2.5-3.5L22 19z" />
      </svg>
    ),
    quote:
      "Our support queue dropped by half in the first month. The team finally spends its hours on the hard problems instead of chasing down status updates all day.",
    author: "Elena Sokolova",
    role: "Head of Customer Success",
    metricValue: "48%",
    metricLabel: "Smaller support queue",
  },
  {
    id: "orbit",
    company: "Orbit Studio",
    accent: "amber",
    mark: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} className="h-full w-full" aria-hidden="true">
        <ellipse cx="12" cy="12" rx="9" ry="4.2" transform="rotate(-20 12 12)" />
        <circle cx="12" cy="12" r="2.6" fill="currentColor" stroke="none" />
      </svg>
    ),
    quote:
      "The forecasting alone paid for the whole contract. We caught a revenue dip six weeks before it would have ever landed on the books, and fixed it in time.",
    author: "Daniel Osei",
    role: "Finance Lead",
    metricValue: "$1.2M",
    metricLabel: "Revenue protected",
  },
  {
    id: "lumen",
    company: "Lumen Retail",
    accent: "sky",
    mark: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" className="h-full w-full" aria-hidden="true">
        <circle cx="12" cy="12" r="3.6" fill="currentColor" stroke="none" />
        <path d="M12 3v2.5M12 18.5V21M3 12h2.5M18.5 12H21M5.6 5.6l1.8 1.8M16.6 16.6l1.8 1.8M18.4 5.6l-1.8 1.8M7.4 16.6l-1.8 1.8" />
      </svg>
    ),
    quote:
      "Every store manager sees the same numbers now. No more spreadsheets emailed at midnight, no more arguing about whose version of the data is the real one.",
    author: "Aiko Tanaka",
    role: "Regional Director",
    metricValue: "340",
    metricLabel: "Stores aligned",
  },
  {
    id: "fathom",
    company: "Fathom",
    accent: "violet",
    mark: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className="h-full w-full" aria-hidden="true">
        <path d="M3 8q3-3 6 0t6 0 6 0" />
        <path d="M3 13q3-3 6 0t6 0 6 0" />
        <path d="M3 18q3-3 6 0t6 0 6 0" />
      </svg>
    ),
    quote:
      "I've rolled out a lot of tools in my career. This is the first one the whole company actually kept using after week two, without anyone having to nag them.",
    author: "Tomás Herrera",
    role: "Chief of Staff",
    metricValue: "96%",
    metricLabel: "Team adoption",
  },
];

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

function Stars({ className }: { className: string }): ReactNode {
  return (
    <div className="flex gap-1" role="img" aria-label="Rated 5 out of 5 stars">
      {[0, 1, 2, 3, 4].map((n) => (
        <svg key={n} viewBox="0 0 20 20" fill="currentColor" className={`h-4 w-4 ${className}`} aria-hidden="true">
          <path d="M10 1.6l2.6 5.3 5.8.9-4.2 4.1 1 5.8L10 15l-5.2 2.7 1-5.8L1.6 7.8l5.8-.9z" />
        </svg>
      ))}
    </div>
  );
}

export default function TstxLogosQuote(): ReactNode {
  const reduce = useReducedMotion();
  const baseId = useId();
  const panelId = `${baseId}-panel`;
  const [activeIndex, setActiveIndex] = useState(0);
  const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);

  const active = TESTIMONIALS[activeIndex];
  const accent = ACCENTS[active.accent];
  const activeTabId = `${baseId}-tab-${activeIndex}`;

  const onKey = (e: KeyboardEvent<HTMLButtonElement>, i: number): void => {
    const len = TESTIMONIALS.length;
    let next = i;
    if (e.key === "ArrowRight" || e.key === "ArrowDown") next = (i + 1) % len;
    else if (e.key === "ArrowLeft" || e.key === "ArrowUp") next = (i - 1 + len) % len;
    else if (e.key === "Home") next = 0;
    else if (e.key === "End") next = len - 1;
    else return;
    e.preventDefault();
    setActiveIndex(next);
    tabRefs.current[next]?.focus();
  };

  return (
    <section className="relative w-full overflow-hidden bg-white py-24 dark:bg-slate-950 sm:py-32">
      <style>{`
        @keyframes tlq-glow-pulse { 0%,100% { opacity:.5; transform:scale(1); } 50% { opacity:.9; transform:scale(1.09); } }
        @keyframes tlq-float { 0%,100% { transform:translateY(0); } 50% { transform:translateY(-6px); } }
        .tlq-glow-a { animation: tlq-glow-pulse 7s ease-in-out infinite; }
        .tlq-glow-b { animation: tlq-glow-pulse 9s ease-in-out infinite reverse; }
        .tlq-float { animation: tlq-float 6s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .tlq-glow-a, .tlq-glow-b, .tlq-float { animation: none !important; }
        }
      `}</style>

      {/* Ambient background */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0">
        <div className="tlq-glow-a absolute -top-24 left-1/4 h-72 w-72 rounded-full bg-gradient-to-br from-indigo-400/25 to-sky-400/15 blur-3xl dark:from-indigo-500/15 dark:to-sky-500/10" />
        <div className="tlq-glow-b absolute -bottom-24 right-1/4 h-72 w-72 rounded-full bg-gradient-to-br from-violet-400/20 to-emerald-400/10 blur-3xl dark:from-violet-500/12 dark:to-emerald-500/8" />
        <div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-slate-200 to-transparent dark:via-slate-800" />
      </div>

      <div className="relative mx-auto max-w-6xl px-6 lg:px-8">
        {/* 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-widest text-slate-600 backdrop-blur dark:border-slate-800 dark:bg-slate-900/70 dark:text-slate-300">
            <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
            Customer stories
          </span>
          <h2 className="mt-6 text-balance text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
            Teams don&rsquo;t just try Cadence. They build their week around it.
          </h2>
          <p className="mt-4 text-pretty text-base leading-relaxed text-slate-600 dark:text-slate-400">
            Six teams, six very different problems, one platform they all rely on. Pick a company to read its story.
          </p>
          <div className="mt-6 flex items-center justify-center gap-3">
            <Stars className="text-amber-500 dark:text-amber-400" />
            <span className="text-sm font-medium text-slate-500 dark:text-slate-400">
              4.9 / 5 &middot; 800+ teams onboarded this year
            </span>
          </div>
        </div>

        {/* Featured quote */}
        <div className="mx-auto mt-14 max-w-4xl">
          <div
            role="tabpanel"
            id={panelId}
            aria-labelledby={activeTabId}
            tabIndex={0}
            className="relative overflow-hidden rounded-3xl border border-slate-200/80 bg-white/80 p-8 shadow-xl shadow-slate-900/5 backdrop-blur-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-800 dark:bg-slate-900/60 dark:shadow-black/30 dark:focus-visible:ring-offset-slate-950 sm:p-12"
          >
            <div
              aria-hidden="true"
              className={`tlq-glow-a pointer-events-none absolute -right-16 -top-16 h-56 w-56 rounded-full bg-gradient-to-br ${accent.glow} blur-3xl transition-colors duration-500`}
            />

            <AnimatePresence mode="wait">
              <motion.div
                key={active.id}
                initial={reduce ? { opacity: 0 } : { opacity: 0, y: 14 }}
                animate={{ opacity: 1, y: 0 }}
                exit={reduce ? { opacity: 0 } : { opacity: 0, y: -14 }}
                transition={{ duration: reduce ? 0.15 : 0.4, ease: "easeOut" }}
                className="relative"
              >
                <div className="flex items-center justify-between gap-4">
                  <svg
                    viewBox="0 0 24 24"
                    fill="currentColor"
                    className={`tlq-float h-11 w-11 ${accent.text}`}
                    aria-hidden="true"
                  >
                    <path d="M7.5 6C5.6 6 4 7.6 4 9.5S5.6 13 7.5 13c.2 0 .3 0 .5-.1-.4 2-2 3.3-4 3.6V19c3.8-.4 6.5-3.4 6.5-7.6C10.5 7.9 9.2 6 7.5 6zm9 0C14.6 6 13 7.6 13 9.5S14.6 13 16.5 13c.2 0 .3 0 .5-.1-.4 2-2 3.3-4 3.6V19c3.8-.4 6.5-3.4 6.5-7.6C19.5 7.9 18.2 6 16.5 6z" />
                  </svg>
                  <Stars className={accent.text} />
                </div>

                <blockquote className="mt-6 text-pretty text-xl font-medium leading-relaxed text-slate-800 dark:text-slate-100 sm:text-2xl">
                  &ldquo;{active.quote}&rdquo;
                </blockquote>

                <div className="mt-8 flex flex-col gap-6 border-t border-slate-200 pt-6 dark:border-slate-800 sm:flex-row sm:items-center sm:justify-between">
                  <div className="flex items-center gap-4">
                    <span
                      className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-gradient-to-br ${accent.avatar} text-sm font-semibold text-white shadow-inner`}
                      aria-hidden="true"
                    >
                      {initials(active.author)}
                    </span>
                    <div>
                      <div className="font-semibold text-slate-900 dark:text-white">{active.author}</div>
                      <div className="text-sm text-slate-500 dark:text-slate-400">
                        {active.role}, {active.company}
                      </div>
                    </div>
                  </div>
                  <div className="sm:text-right">
                    <div className={`text-2xl font-bold tracking-tight ${accent.text}`}>{active.metricValue}</div>
                    <div className="text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
                      {active.metricLabel}
                    </div>
                  </div>
                </div>
              </motion.div>
            </AnimatePresence>
          </div>
        </div>

        {/* Logo strip / tablist */}
        <div className="mt-12">
          <p className="text-center text-xs font-semibold uppercase tracking-widest text-slate-400 dark:text-slate-500">
            Trusted by teams at
          </p>
          <div
            role="tablist"
            aria-label="Customer companies"
            aria-orientation="horizontal"
            className="mx-auto mt-6 grid max-w-4xl grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6"
          >
            {TESTIMONIALS.map((t, i) => {
              const selected = i === activeIndex;
              const a = ACCENTS[t.accent];
              return (
                <button
                  key={t.id}
                  ref={(el) => {
                    tabRefs.current[i] = el;
                  }}
                  type="button"
                  role="tab"
                  id={`${baseId}-tab-${i}`}
                  aria-selected={selected}
                  aria-controls={panelId}
                  tabIndex={selected ? 0 : -1}
                  onClick={() => setActiveIndex(i)}
                  onKeyDown={(e) => onKey(e, i)}
                  className={`flex items-center justify-center gap-2.5 rounded-xl border px-3 py-3.5 text-sm font-medium transition duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950 ${
                    selected
                      ? `border-transparent ${a.chip} ${a.text} shadow-sm ring-2 ${a.ring}`
                      : "border-slate-200/70 bg-white/50 text-slate-500 opacity-70 hover:border-slate-300 hover:text-slate-700 hover:opacity-100 dark:border-slate-800 dark:bg-slate-900/40 dark:text-slate-400 dark:hover:border-slate-700 dark:hover:text-slate-200"
                  }`}
                >
                  <span className="h-5 w-5 shrink-0">{t.mark}</span>
                  <span className="whitespace-nowrap">{t.company}</span>
                </button>
              );
            })}
          </div>
        </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