Web InnoventixFreeCode

Single Large Testimonial

Original · free

single large quote

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

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

type Testimonial = {
  id: string;
  quote: string;
  highlight: string;
  name: string;
  role: string;
  initials: string;
  avatar: string;
};

const TESTIMONIALS: Testimonial[] = [
  {
    id: "priya",
    quote:
      "We cut our monthly close from eleven days to three. The first time finance and ops read the same numbers without arguing over whose spreadsheet was right, I knew Beacon had already paid for itself.",
    highlight: "eleven days to three",
    name: "Priya Nadesan",
    role: "VP of Finance, B2B logistics scale-up",
    initials: "PN",
    avatar: "from-indigo-500 to-violet-500",
  },
  {
    id: "marcus",
    quote:
      "I stopped dreading Monday reporting. Beacon flags the one metric that's quietly drifting before it turns into a fire, so my team spends the week fixing problems instead of hunting for them.",
    highlight: "before it turns into a fire",
    name: "Marcus Ochoa",
    role: "Head of Growth, consumer subscription app",
    initials: "MO",
    avatar: "from-emerald-500 to-sky-500",
  },
  {
    id: "dana",
    quote:
      "Onboarding took an afternoon, not a quarter. Six weeks in, every manager builds their own dashboards and I haven't fielded a single 'can you pull this for me' request since.",
    highlight: "an afternoon, not a quarter",
    name: "Dana Whitfield",
    role: "Director of Operations, healthcare startup",
    initials: "DW",
    avatar: "from-rose-500 to-amber-500",
  },
];

function renderQuote(quote: string, highlight: string): ReactNode {
  const idx = quote.indexOf(highlight);
  if (idx === -1) return quote;
  return (
    <>
      {quote.slice(0, idx)}
      <span className="relative whitespace-nowrap font-semibold text-indigo-600 dark:text-indigo-300">
        <span className="relative z-10">{highlight}</span>
        <span
          aria-hidden="true"
          className="absolute inset-x-0 bottom-1 z-0 h-3 -skew-x-6 rounded-sm bg-indigo-200/60 dark:bg-indigo-400/20"
        />
      </span>
      {quote.slice(idx + highlight.length)}
    </>
  );
}

function Stars(): ReactNode {
  return (
    <div
      role="img"
      aria-label="Rated 5 out of 5 stars"
      className="flex items-center justify-center gap-1"
    >
      {[0, 1, 2, 3, 4].map((i) => (
        <svg
          key={i}
          viewBox="0 0 20 20"
          fill="currentColor"
          aria-hidden="true"
          className="h-5 w-5 text-amber-400 drop-shadow-sm"
        >
          <path d="M9.05 2.93c.3-.92 1.6-.92 1.9 0l1.36 4.18a1 1 0 0 0 .95.69h4.4c.97 0 1.37 1.24.59 1.81l-3.56 2.59a1 1 0 0 0-.36 1.12l1.36 4.18c.3.92-.76 1.69-1.54 1.12l-3.56-2.58a1 1 0 0 0-1.18 0l-3.56 2.58c-.78.57-1.84-.2-1.54-1.12l1.36-4.18a1 1 0 0 0-.36-1.12L1.4 9.6c-.78-.57-.38-1.81.59-1.81h4.4a1 1 0 0 0 .95-.69L9.05 2.93Z" />
        </svg>
      ))}
    </div>
  );
}

export default function TstxSingleLarge() {
  const [active, setActive] = useState(0);
  const reduce = useReducedMotion();
  const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
  const current = TESTIMONIALS[active];

  function onKeyDown(event: KeyboardEvent<HTMLButtonElement>, index: number) {
    const count = TESTIMONIALS.length;
    let next = index;
    if (event.key === "ArrowRight" || event.key === "ArrowDown") {
      next = (index + 1) % count;
    } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
      next = (index - 1 + count) % count;
    } else if (event.key === "Home") {
      next = 0;
    } else if (event.key === "End") {
      next = count - 1;
    } else {
      return;
    }
    event.preventDefault();
    setActive(next);
    tabRefs.current[next]?.focus();
  }

  return (
    <section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 to-white px-6 py-24 sm:py-32 dark:from-slate-950 dark:to-slate-900">
      <style>{`
        @keyframes tstxsl-float-a {
          0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
          50% { transform: translate3d(24px, -28px, 0) scale(1.08); }
        }
        @keyframes tstxsl-float-b {
          0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
          50% { transform: translate3d(-30px, 20px, 0) scale(1.12); }
        }
        @keyframes tstxsl-shimmer {
          0% { background-position: 0% 50%; }
          100% { background-position: 200% 50%; }
        }
        .tstxsl-orb-a { animation: tstxsl-float-a 16s ease-in-out infinite; }
        .tstxsl-orb-b { animation: tstxsl-float-b 21s ease-in-out infinite; }
        .tstxsl-mark {
          background-image: linear-gradient(100deg, #6366f1, #a855f7, #38bdf8, #6366f1);
          background-size: 200% 100%;
          -webkit-background-clip: text;
          background-clip: text;
          -webkit-text-fill-color: transparent;
          color: transparent;
          animation: tstxsl-shimmer 9s linear infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .tstxsl-orb-a, .tstxsl-orb-b, .tstxsl-mark { animation: none !important; }
        }
      `}</style>

      {/* Atmospheric background */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
        <div className="tstxsl-orb-a absolute -left-24 top-4 h-80 w-80 rounded-full bg-indigo-400/25 blur-3xl dark:bg-indigo-500/20" />
        <div className="tstxsl-orb-b absolute -right-20 bottom-0 h-96 w-96 rounded-full bg-violet-400/20 blur-3xl dark:bg-violet-600/20" />
        <div
          className="absolute inset-0 opacity-[0.4] dark:opacity-[0.25]"
          style={{
            backgroundImage:
              "radial-gradient(circle at 1px 1px, rgb(100 116 139 / 0.18) 1px, transparent 0)",
            backgroundSize: "28px 28px",
            maskImage:
              "radial-gradient(ellipse 70% 60% at 50% 40%, black, transparent 75%)",
            WebkitMaskImage:
              "radial-gradient(ellipse 70% 60% at 50% 40%, black, transparent 75%)",
          }}
        />
      </div>

      <div className="relative mx-auto flex max-w-3xl flex-col items-center text-center">
        {/* Eyebrow */}
        <span className="inline-flex items-center gap-2 rounded-full border border-slate-200/80 bg-white/70 px-4 py-1.5 text-xs font-semibold uppercase tracking-[0.18em] text-slate-600 shadow-sm backdrop-blur dark:border-slate-700/70 dark:bg-slate-800/60 dark:text-slate-300">
          <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4 text-indigo-500 dark:text-indigo-400">
            <path
              d="M12 3l1.9 4.6L18.5 9l-4.6 1.4L12 15l-1.9-4.6L5.5 9l4.6-1.4L12 3z"
              fill="currentColor"
            />
          </svg>
          Customer story
        </span>

        {/* Giant decorative quote mark */}
        <span
          aria-hidden="true"
          className="tstxsl-mark pointer-events-none mt-8 select-none font-serif text-[7rem] leading-[0.6] sm:text-[9rem]"
        >
          &ldquo;
        </span>

        {/* Quote panel */}
        <div className="mt-2 w-full" role="tabpanel" id="tstxsl-panel" aria-live="polite">
          <AnimatePresence mode="wait">
            <motion.figure
              key={current.id}
              initial={reduce ? { opacity: 1 } : { opacity: 0, y: 18 }}
              animate={{ opacity: 1, y: 0 }}
              exit={reduce ? { opacity: 1 } : { opacity: 0, y: -18 }}
              transition={{ duration: reduce ? 0 : 0.5, ease: "easeOut" }}
              className="flex flex-col items-center"
            >
              <blockquote className="text-balance text-2xl font-medium leading-snug tracking-tight text-slate-800 sm:text-3xl lg:text-4xl dark:text-slate-100">
                {renderQuote(current.quote, current.highlight)}
              </blockquote>

              <div className="mt-8">
                <Stars />
              </div>

              <figcaption className="mt-6 flex items-center gap-4">
                <span
                  className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-gradient-to-br ${current.avatar} text-sm font-bold text-white shadow-lg ring-2 ring-white/70 dark:ring-slate-900/70`}
                  aria-hidden="true"
                >
                  {current.initials}
                </span>
                <span className="text-left">
                  <span className="block text-base font-semibold text-slate-900 dark:text-white">
                    {current.name}
                  </span>
                  <span className="block text-sm text-slate-500 dark:text-slate-400">
                    {current.role}
                  </span>
                </span>
              </figcaption>
            </motion.figure>
          </AnimatePresence>
        </div>

        {/* Pager / tablist */}
        <div
          role="tablist"
          aria-label="Featured customer testimonials"
          className="mt-12 flex items-center gap-3"
        >
          {TESTIMONIALS.map((t, i) => {
            const selected = i === active;
            return (
              <button
                key={t.id}
                ref={(el) => {
                  tabRefs.current[i] = el;
                }}
                type="button"
                role="tab"
                id={`tstxsl-tab-${t.id}`}
                aria-selected={selected}
                aria-controls="tstxsl-panel"
                aria-label={`Show testimonial from ${t.name}`}
                tabIndex={selected ? 0 : -1}
                onClick={() => setActive(i)}
                onKeyDown={(e) => onKeyDown(e, i)}
                className={`group relative flex h-11 w-11 items-center justify-center rounded-full outline-none transition 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-900 ${
                  selected ? "scale-105" : "opacity-55 hover:opacity-100"
                }`}
              >
                <span
                  className={`flex h-full w-full items-center justify-center rounded-full bg-gradient-to-br ${t.avatar} text-xs font-bold text-white shadow-md transition ${
                    selected
                      ? "ring-2 ring-indigo-500 ring-offset-2 ring-offset-white dark:ring-offset-slate-900"
                      : ""
                  }`}
                >
                  {t.initials}
                </span>
              </button>
            );
          })}
        </div>

        {/* Trust line */}
        <p className="mt-8 text-sm text-slate-500 dark:text-slate-400">
          <span className="font-semibold text-slate-700 dark:text-slate-200">4.9 / 5</span>{" "}
          average rating across{" "}
          <span className="font-semibold text-slate-700 dark:text-slate-200">1,200+</span>{" "}
          teams running on Beacon
        </p>
      </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

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