Skip to content
All Taste components

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

Customers

Teams that stopped chasing invoices

“Month-end used to eat my whole first week. Now it’s a Tuesday afternoon.”

IR
Ishita RaoFinance lead, Kestrel Studio

“The reminders sound like me on a good day. Clients actually reply to them.”

DO
Daniel OkaforFounder, Brightwater Design

“We stopped exporting CSVs to reconcile. That alone paid for it.”

MT
Mei TanakaOps manager, Larkspur Labs

“Our accountant asked what changed. The books were simply clean.”

AM
Arjun MehtaOwner, Oakline Joinery

“Getting paid in four currencies without four spreadsheets is the dream.”

SM
Sofia MarínFreelance translator

“Set it up in an evening. The first late invoice was paid by Friday.”

KA
Kwame AsanteDirector, Tidewell Films

“Every payment lands matched to its invoice. I just glance and approve.”

PN
Priya NairController, Halcyon Health

“The yearly view finally shows which clients are worth keeping.”

TF
Tom FischerPartner, Meridian Studio

“Our team of three closes the books faster than our old team of six.”

LH
Leila HaddadCOO, Quarry & Co

“I trust the numbers on the dashboard. That is new for me.”

RI
Ravi IyerFounder, Blue Pine Supply
/* 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.