Animated sections
Testimonial marquee
Two rows of customer quotes drifting in opposite directions, pausing when someone stops to read.
2 rows · opposite directions · different speeds · pause on hover · 10 quotes
Teams that stopped chasing invoices
“Month-end used to eat my whole first week. Now it’s a Tuesday afternoon.”
“The reminders sound like me on a good day. Clients actually reply to them.”
“We stopped exporting CSVs to reconcile. That alone paid for it.”
“Our accountant asked what changed. The books were simply clean.”
“Getting paid in four currencies without four spreadsheets is the dream.”
“Set it up in an evening. The first late invoice was paid by Friday.”
“Every payment lands matched to its invoice. I just glance and approve.”
“The yearly view finally shows which clients are worth keeping.”
“Our team of three closes the books faster than our old team of six.”
“I trust the numbers on the dashboard. That is new for me.”
/* Add once to globals.css:
@keyframes marquee { to { transform: translateX(-50%); } }
*/
const quotes = [
{ quote: "Month-end used to eat my whole first week. Now it’s a Tuesday afternoon.", name: "Ishita Rao", role: "Finance lead, Kestrel Studio" },
{ quote: "The reminders sound like me on a good day. Clients actually reply to them.", name: "Daniel Okafor", role: "Founder, Brightwater Design" },
{ quote: "We stopped exporting CSVs to reconcile. That alone paid for it.", name: "Mei Tanaka", role: "Ops manager, Larkspur Labs" },
{ quote: "Our accountant asked what changed. The books were simply clean.", name: "Arjun Mehta", role: "Owner, Oakline Joinery" },
{ quote: "Getting paid in four currencies without four spreadsheets is the dream.", name: "Sofia Marín", role: "Freelance translator" },
{ quote: "Set it up in an evening. The first late invoice was paid by Friday.", name: "Kwame Asante", role: "Director, Tidewell Films" },
{ quote: "Every payment lands matched to its invoice. I just glance and approve.", name: "Priya Nair", role: "Controller, Halcyon Health" },
{ quote: "The yearly view finally shows which clients are worth keeping.", name: "Tom Fischer", role: "Partner, Meridian Studio" },
{ quote: "Our team of three closes the books faster than our old team of six.", name: "Leila Haddad", role: "COO, Quarry & Co" },
{ quote: "I trust the numbers on the dashboard. That is new for me.", name: "Ravi Iyer", role: "Founder, Blue Pine Supply" },
];
const rows = [quotes.slice(0, 5), quotes.slice(5)];
const initials = (name: string) => name.split(" ").map((w) => w[0]).join("").slice(0, 2);
export function TestimonialMarquee() {
return (
<section aria-labelledby="tm-title" className="overflow-hidden bg-surface py-20 font-sans">
<div className="mb-10 px-4 text-center">
<p className="text-xs font-medium text-fg-subtle animate-rise motion-reduce:animate-none">Customers</p>
<h2 id="tm-title" className="mt-2 text-balance text-3xl font-medium tracking-tight text-fg animate-rise [animation-delay:70ms] motion-reduce:animate-none">
Teams that stopped chasing invoices
</h2>
</div>
<div className="grid gap-3.5">
{rows.map((row, r) => (
<div key={r} className="group overflow-hidden [mask-image:linear-gradient(90deg,transparent,var(--tst-fg)_12%,var(--tst-fg)_88%,transparent)]">
<div
className={
"flex w-max group-hover:[animation-play-state:paused] motion-reduce:animate-none " +
(r === 0 ? "animate-[marquee_44s_linear_infinite]" : "animate-[marquee_52s_linear_infinite_reverse]")
}
>
{[...row, ...row].map((q, i) => (
<figure key={i} aria-hidden={i >= row.length || undefined} className="mr-3.5 grid w-[252px] shrink-0 content-between gap-4 rounded-lg border border-border bg-bg p-4 transition-colors hover:border-border-strong sm:w-[300px]">
<blockquote className="text-sm leading-relaxed text-fg">“{q.quote}”</blockquote>
<figcaption className="flex items-center gap-2.5 text-[13px]">
<span className="grid size-7 place-items-center rounded-full bg-primary-soft text-[11px] font-medium text-primary-soft-fg">{initials(q.name)}</span>
<span>
<b className="block font-medium text-fg">{q.name}</b>
<span className="text-fg-subtle">{q.role}</span>
</span>
</figcaption>
</figure>
))}
</div>
</div>
))}
</div>
</section>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.