Fading Quote Testimonials
A testimonial slider that cross-fades one large quote at a time beside a photo, with a progress bar, arrows and pause on hover. React and Tailwind CSS.
FadingQuoteTestimonials.tsx
"use client";
// Dark band: a photo beside one large quote at a time, cross-fading every few
// seconds, with a progress bar and arrows. Pauses while the pointer is over it.
// Pure Tailwind, no dependencies. Works with Tailwind v3 and v4.
import { useEffect, useState } from "react";
type Testimonial = { quote: string; name: string; detail?: string };
type Props = {
image?: string;
imageAlt?: string;
testimonials?: Testimonial[];
interval?: number;
};
const DEFAULT_TESTIMONIALS: Testimonial[] = [
{ quote: "The most effortless week of our lives. Every detail had been thought of before we knew we wanted it.", name: "Charlotte & Henry W.", detail: "London · Amalfi Coast" },
{ quote: "We have chartered for twenty years. These crews are simply in a different league: warm, discreet and unfailingly good.", name: "Alexandre D.", detail: "Geneva · The Maldives" },
{ quote: "They found us the right yacht, negotiated beautifully and stayed with us long after the sale. A true partner.", name: "Marcus L.", detail: "Yacht owner" },
{ quote: "Three generations on board and not one complaint. The children still talk about the night fishing.", name: "The Okonkwo family", detail: "Lagos & New York · Dalmatian Coast" },
];
const Arrow = ({ flip = false }: { flip?: boolean }) => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.1} aria-hidden className={`h-4 w-4 ${flip ? "rotate-180" : ""}`}>
<path d="M3 12h17M14 6l6 6-6 6" />
</svg>
);
export default function FadingQuoteTestimonials({
image = "https://images.unsplash.com/photo-1541972184324-d7e22e823db4?w=1600&q=80&auto=format&fit=crop",
imageAlt = "Two people raising glasses at sunset",
testimonials = DEFAULT_TESTIMONIALS,
interval = 7000,
}: Props) {
const [index, setIndex] = useState(0);
const [paused, setPaused] = useState(false);
const count = testimonials.length;
useEffect(() => {
if (paused || count < 2) return;
const id = setInterval(() => setIndex((i) => (i + 1) % count), interval);
return () => clearInterval(id);
}, [paused, count, interval]);
if (!count) return null;
const go = (step: number) => setIndex((i) => (i + step + count) % count);
return (
<section
className="grid bg-[#0b131a] font-['Helvetica_Neue',Helvetica,Arial,sans-serif] text-white antialiased lg:grid-cols-12"
onPointerEnter={() => setPaused(true)}
onPointerLeave={() => setPaused(false)}
>
<div className="relative aspect-[4/3] lg:col-span-5 lg:aspect-auto">
<img src={image} alt={imageAlt} className="absolute inset-0 h-full w-full object-cover" />
</div>
<div className="flex flex-col justify-between gap-16 px-4 py-20 md:px-12 md:py-28 lg:col-span-7 lg:px-20">
<div className="grid" aria-live="polite">
{testimonials.map((t, i) => (
<figure
key={t.name}
aria-hidden={i !== index}
className={`col-start-1 row-start-1 transition duration-[1200ms] ease-[cubic-bezier(0.22,1,0.36,1)] ${
i === index ? "translate-y-0 opacity-100" : "pointer-events-none translate-y-6 opacity-0"
}`}
>
<blockquote className="text-[clamp(1.5rem,2.1vw,2.25rem)] leading-[1.15] tracking-[-0.02em] md:text-[2.6rem] md:leading-[1.2]">
“{t.quote}”
</blockquote>
<figcaption className="mt-10 flex flex-col gap-1">
<span className="text-xs font-medium uppercase tracking-[0.08em]">{t.name}</span>
{t.detail && <span className="text-sm text-white/55">{t.detail}</span>}
</figcaption>
</figure>
))}
</div>
{count > 1 && (
<div className="flex items-center gap-8">
<p className="text-sm tabular-nums text-white/55">
{String(index + 1).padStart(2, "0")} / {String(count).padStart(2, "0")}
</p>
<div className="relative h-px flex-1 bg-white/15">
<div
className="absolute inset-y-0 left-0 bg-[#dcc7a0] transition-[width] duration-700 ease-[cubic-bezier(0.22,1,0.36,1)]"
style={{ width: `${((index + 1) / count) * 100}%` }}
/>
</div>
<div className="flex gap-3">
<button type="button" onClick={() => go(-1)} aria-label="Previous testimonial" className="grid h-12 w-12 place-items-center rounded-full border border-white/25 transition-colors hover:bg-white hover:text-[#0d1114]">
<Arrow flip />
</button>
<button type="button" onClick={() => go(1)} aria-label="Next testimonial" className="grid h-12 w-12 place-items-center rounded-full border border-white/25 transition-colors hover:bg-white hover:text-[#0d1114]">
<Arrow />
</button>
</div>
</div>
)}
</div>
</section>
);
}
How to use it
- Make sure your Next.js or React project has Tailwind CSS set up.
- Create
components/FadingQuoteTestimonials.tsxand paste the code. - Import it into a page and replace the placeholder text and images with your own.
More free components
All testimonials →
Ready for a faster site that's easier to edit?
Book a free 30-minute call. We'll review your current site and send a fixed-price plan. No pressure, no pitch.
Book Free Call No commitment required

