DHxWhy

Highlight Testimonial Section

Testimonials 20

About

Highlight testimonial section for React and Tailwind CSS (shadcn/ui) that sweeps a highlight across the key phrase of each customer quote and lifts that phrase into the section headline. It is built to let visitors read why customers are happy in one line before they read the full quote. A testimonial section is a social-proof block that displays customer quotes with names and roles; this one takes testimonials with a quote, a highlight phrase, an author, a role and an optional avatar, wraps the phrase in a mark element, animates the highlight with a background-size sweep and promotes it into the headline with a FLIP translate and scale animation using the Web Animations API. Supports controlled and uncontrolled index, an optional auto-rotate interval that pauses on hover and focus, dot and arrow navigation with arrow keys, carousel ARIA semantics with a polite live headline, light and dark highlight tokens from the shadcn chart palette, and a reduced motion fallback. Use it on landing pages, pricing pages, case studies and onboarding screens. The included demo rotates three sample quotes and makes no requests. Requires React and lucide-react, with no animation library.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Highlight Testimonial Section to your project and fits it to your theme.

Code

How Highlight Testimonial Section is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import * as React from "react";
import { RotateCcw } from "lucide-react";
import { HighlightTestimonialSection } from "@/components/ui/highlight-testimonial-section";
import type { Testimonial } from "@/components/ui/highlight-testimonial-section";

type DemoProps = { autoplay?: boolean; bare?: boolean; index?: number; interval?: number };

const TESTIMONIALS: Testimonial[] = [
  { quote: "We moved three teams over in a week. Onboarding took minutes, not days, and nobody asked for the old tool back.", highlight: "Onboarding took minutes, not days", author: "Maria Lopez", role: "Head of Platform, Acme", avatar: "https://cdn.21st.dev/assets/mirror/1b/1bd1e03a7e1a1548cdab4819240bd550a5daab2767344e0e302833defe91f142.jpg" },
  { quote: "The dashboards finally answer the questions our execs ask. Reports that used to take an afternoon now take a click.", highlight: "Reports that used to take an afternoon now take a click", author: "Jon Park", role: "Data lead, Northwind", avatar: "https://cdn.21st.dev/assets/mirror/d2/d2996fcdda8bb515128ec84aa81468b77bbb31c13b7de0847dc95a5e5b34a90f.jpg" },
  { quote: "Support replied in under ten minutes with an actual fix, twice. That is the main reason we renewed.", highlight: "replied in under ten minutes with an actual fix", author: "Aisha Rahman", role: "CTO, Fernwood", avatar: "https://cdn.21st.dev/assets/mirror/cc/cc21fdbbb78a68b33c08747bc6566e1f1d28f604f191fa5a6ea344e0f7ece19e.jpg" },
];

export default function HighlightTestimonialSectionDemo({ autoplay = true, bare = false, index: preset, interval = 2500 }: DemoProps) {
  const [index, setIndex] = React.useState(preset ?? 0);
  const [run, setRun] = React.useState(0);
  const [motion, setMotion] = React.useState({ ready: false, reduced: true });

  React.useEffect(() => {
    const media = window.matchMedia("(prefers-reduced-motion: reduce)");
    const update = () => setMotion({ ready: true, reduced: media.matches });
    update();
    media.addEventListener("change", update);
    return () => media.removeEventListener("change", update);
  }, []);

  return (
    <main className="flex min-h-svh w-full flex-col items-center justify-center gap-5 bg-background px-5 py-6 text-foreground">
      {!bare && (
        <header className="w-full max-w-[460px]">
          <p className="mb-1 text-[10px] font-medium uppercase tracking-[0.2em] text-muted-foreground">Social proof, one line at a time</p>
          <h1 className="text-[24px] font-semibold leading-tight tracking-tight">The best phrase becomes the headline.</h1>
        </header>
      )}
      <div className="w-full max-w-[460px]">
        <HighlightTestimonialSection key={run} testimonials={TESTIMONIALS} index={index} onIndexChange={setIndex} interval={autoplay && motion.ready && !motion.reduced ? interval : 0} />
      </div>
      {!bare && (
        <footer className="flex w-full max-w-[460px] items-center justify-between text-[10px] text-muted-foreground">
          <span>Sample quotes · hover to pause, arrows to browse</span>
          <button type="button" data-replay onClick={() => { setIndex(0); setRun((previous) => previous + 1); }} className="inline-flex items-center gap-1 rounded px-1 underline decoration-border underline-offset-4 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><RotateCcw className="size-3" aria-hidden="true" />Replay</button>
        </footer>
      )}
    </main>
  );
}

Dependencies

More by DHxWhy

View all
  • Sun Moon Dark Mode Theme Toggle

    Sun Moon Dark Mode Theme Toggle

    186
  • Country Flags Globe

    Country Flags Globe

    110
  • Live Notification Feed

    Live Notification Feed

    109
  • Airplane Window Gallery

    Airplane Window Gallery

    95
  • Domino Scroll Image Gallery

    Domino Scroll Image Gallery

    93
  • QR Code Scan Login Form with Password Fallback

    QR Code Scan Login Form with Password Fallback

    82
  • Constellation Image Gallery

    Constellation Image Gallery

    80
  • Ferris Wheel Image Gallery

    Ferris Wheel Image Gallery

    73

More in Testimonials

View all
  • Testimonial

    Testimonial

    117
  • testimonial

    testimonial

    797
  • Staggered Testimonials Grid

    Staggered Testimonials Grid

    143
  • Testimonial 2

    Testimonial 2

    32
  • Voice Testimonial

    Voice Testimonial

    120
  • Testimonial Card

    Testimonial Card

    69
  • Split Sign In Form

    Split Sign In Form

    16
  • Testimonials Marquee

    Testimonials Marquee

    574