All-Access · 23 templates$1319.97$99/year — save 92%Get access
  • HomeHome
  • TemplatesTemplates17+
  • ComponentsComponents80+
  • PricingPricing
  • Need a Website?Need a Website?New
Unlock All AccessUnlock All Access

Our latest template collection

Ready-to-launch sites for real estate agents, service businesses, and local brands — built to rank, load fast, and convert.

Explore All TemplatesExplore All Templates

Orvaine - Luxury Equestrian & Horse Breeding Website Template

Orvaine is a premium website template for stud farms, sport horse breeders and equestrian estates: horse pages with pedigrees, events, films. Next.js, Astro, Sanity.

Download Template
Orvaine - Luxury Equestrian & Horse Breeding Website Template

Maressa - Luxury Yacht Charter & Sales Website Template

Maressa is a premium website template for yacht charter, brokerage and superyacht brands: 21 editable sections, full-screen films, Next.js and Astro versions. Sanity.

Download Template
Maressa - Luxury Yacht Charter & Sales Website Template

Krishi – Agriculture & Farm Website Template

Krishi is a premium website template for farms, agritech and food brands: 17 editable sections, cinematic GSAP motion and both Next.js and Astro versions. Built with Sanity.

Download Template
Krishi – Agriculture & Farm Website Template
ComponentsTestimonialsFading Quote Testimonials
FreeReactTailwind CSSNext.js

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.

View codeView code
Full screen
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]">
                &ldquo;{t.quote}&rdquo;
              </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

  1. Make sure your Next.js or React project has Tailwind CSS set up.
  2. Create components/FadingQuoteTestimonials.tsx and paste the code.
  3. Import it into a page and replace the placeholder text and images with your own.
Maressa - Luxury Yacht Charter & Sales Website Template
From the full template
Maressa - Luxury Yacht Charter & Sales Website Template$79

This section is one part of Maressa - Luxury Yacht Charter & Sales Website Template: a complete website with every page, Sanity CMS and SEO already set up.

Get the full template Get the full template Live preview Live preview

More free components

All testimonials →
Press Marquee

Press Marquee

Testimonial-classic-circle

Testimonial-classic-circle

ConveyorBelt Slider

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 Book Free Call No commitment required

Fast Next.js + Sanity websites, built for you or ready to launch with our templates.

Book a Free Call Book a Free Call
Services
  • Web Design
  • UI/UX Design
  • Framer Dev
  • Webflow Dev
  • Custom Dev
  • Sanity CMS
Resources
  • Templates
  • Pricing
  • License
  • Components
  • Case Studies
Company
  • About Us
  • Contact
  • Privacy Policy
© 2026 rayso.studio — All rights reservedDesigned & built in-house Remote-first agency
Let's TalkLet's Talk