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>
);
}
















