Auto-rotating quote
A timed quote block with a dot-progress bar that stretches into a pill
npx shadcn@latest add https://ethankd.com/r/rotating-testimonials.jsonThey treated our shipment like it was their own cargo.
"use client";
import { useEffect, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
const TESTIMONIALS = [
{ quote: "They treated our shipment like it was their own cargo.", name: "Amara Chen", role: "Ops Lead, Northline Freight" },
{ quote: "Clearance that used to take days now takes hours.", name: "Devon Reyes", role: "Founder, Reyes Import Co." },
{ quote: "The one partner who calls before something goes wrong, not after.", name: "Priya Nair", role: "Supply Chain Director" },
];
/**
* Auto-rotating quote block: a decorative quote-mark glyph, a fade-rise
* entrance on the blockquote itself, and a dot-progress tab bar where the
* active dot stretches into a pill rather than just changing colour.
*/
export function Testimonials() {
const [i, setI] = useState(0);
useEffect(() => {
const id = setInterval(() => setI((v) => (v + 1) % TESTIMONIALS.length), 6000);
return () => clearInterval(id);
}, []);
const t = TESTIMONIALS[i];
return (
<figure className="relative max-w-lg">
<svg width="40" height="40" viewBox="0 0 40 40" className="text-[#c79a3e]" aria-hidden>
<path
d="M0 24C0 12 8 4 18 4v6c-6 0-9 4-9 9h9v17H0V24Zm22 0C22 12 30 4 40 4v6c-6 0-9 4-9 9h9v17H22V24Z"
fill="currentColor"
/>
</svg>
<AnimatePresence mode="wait">
<motion.blockquote
key={i}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
className="mt-4 text-[1.6rem] leading-snug text-[#1a1513]"
>
{t.quote}
</motion.blockquote>
</AnimatePresence>
<figcaption className="mt-6">
<span className="block font-semibold text-[#6e1019]">{t.name}</span>
<span className="text-sm text-[#5a4f49]">{t.role}</span>
</figcaption>
<div className="mt-8 flex gap-2" role="tablist" aria-label="Testimonials">
{TESTIMONIALS.map((_, idx) => (
<button
key={idx}
role="tab"
aria-selected={idx === i}
aria-label={`Testimonial ${idx + 1}`}
onClick={() => setI(idx)}
className={`h-1.5 rounded-full transition-all ${idx === i ? "w-8 bg-[#6e1019]" : "w-2 bg-[#e8e0d6] hover:bg-[#e7d3a6]"}`}
/>
))}
</div>
</figure>
);
}
Playground
They treated our shipment like it was their own cargo.