# Testimonials Marquee Columns — React Testimonial Section

> Three columns of customer quotes drift vertically in opposite directions beside a fixed headline; hovering a column pauses it.

- Category: [Testimonials](https://pixelpapa.com/components/testimonials.md)
- Tags: testimonials, marquee, columns, infinite, social-proof
- Dependencies: lucide-react (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/testimonials/testimonials-marquee-columns

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/testimonials-marquee-columns.json"
```

The file is written to `components/pixelpapa/testimonials-marquee-columns.tsx`. To install manually, run `npm install lucide-react` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { ArrowRight, Star } from "lucide-react";
import type { CSSProperties } from "react";
import { cn } from "@/lib/utils";

type Testimonial = { name: string; role: string; body: string };

const tones = [
  "from-sky-400 to-indigo-500",
  "from-rose-400 to-orange-400",
  "from-emerald-400 to-teal-600",
  "from-violet-400 to-fuchsia-500",
  "from-amber-400 to-rose-500",
  "from-cyan-400 to-blue-600",
];

const defaultTestimonials: Testimonial[] = [
  { name: "Imani Okafor", role: "Staff Engineer, Lumen Freight", body: "We cut our release checklist from 41 steps to 6. The other 35 just happen now." },
  { name: "Tomás Rivera", role: "Head of Mobile, Parcelbird", body: "First tool our QA lead asked to keep after the trial ended. That has never happened before." },
  { name: "Hana Sato", role: "CTO, Kindling", body: "Staged rollouts used to be a spreadsheet and a prayer. Cadence halts a bad build before I've finished my coffee." },
  { name: "Elliot Brandt", role: "Engineering Manager, Odefy", body: "Friday releases stopped being a joke in our retro." },
  { name: "Priya Venkataraman", role: "Release Lead, Mosaic Health", body: "The audit trail alone saved us two weeks during our SOC 2 renewal. Every approval, every rollback, timestamped and exportable." },
  { name: "Jonas Lindqvist", role: "iOS Lead, Fjord Bank", body: "I got my Thursday nights back." },
  { name: "Amara Diallo", role: "VP Engineering, Tessel", body: "We went from one release a fortnight to nine a week without adding a single person to the on-call rota." },
  { name: "Wren Castellanos", role: "Product Manager, Slate & Co", body: "I can finally answer “is it live yet?” without pinging three engineers." },
  { name: "Dmitri Volkov", role: "Android Engineer, Routewise", body: "The phased rollout graph is the best piece of UI I look at all day." },
  { name: "Sofia Marchetti", role: "QA Director, Bellhop", body: "Crash-free sessions went from 98.2% to 99.7% in a single quarter. We changed nothing else." },
  { name: "Kwame Mensah", role: "Founder, Pocketful", body: "Set it up on a Tuesday afternoon. Shipped with it on Wednesday morning." },
  { name: "Nora Feld", role: "Platform Engineer, Greenroom", body: "Rollbacks take eleven seconds. I timed it twice because I didn't believe it the first time." },
];

function initials(name: string) {
  return name
    .split(" ")
    .map((part) => part[0])
    .slice(0, 2)
    .join("");
}

function QuoteCard({ item, tone }: { item: Testimonial; tone: string }) {
  return (
    <figure className="rounded-2xl border border-border bg-card p-5 shadow-sm shadow-foreground/[0.03] transition-colors duration-300 hover:border-primary/40">
      <blockquote className="text-[0.94rem] leading-relaxed text-pretty text-card-foreground">{item.body}</blockquote>
      <figcaption className="mt-4 flex items-center gap-3">
        <span
          aria-hidden
          className={cn(
            "flex size-9 shrink-0 items-center justify-center rounded-full bg-linear-to-br text-xs font-semibold text-white",
            tone,
          )}
        >
          {initials(item.name)}
        </span>
        <span className="min-w-0">
          <span className="block truncate text-sm font-medium text-card-foreground">{item.name}</span>
          <span className="block truncate text-xs text-muted-foreground">{item.role}</span>
        </span>
      </figcaption>
    </figure>
  );
}

function MarqueeColumn({
  items,
  offset,
  duration,
  reverse,
  className,
}: {
  items: Testimonial[];
  offset: number;
  duration: number;
  reverse?: boolean;
  className?: string;
}) {
  return (
    <div className={cn("pp-testimonials-marquee-columns-col h-full overflow-hidden", className)}>
      {/* The list is rendered twice; sliding by exactly -50% lands on an identical frame. */}
      <ul
        className="pp-testimonials-marquee-columns-track flex flex-col gap-4 pb-4"
        style={{ "--pp-d": `${duration}s`, "--pp-dir": reverse ? "reverse" : "normal" } as CSSProperties}
      >
        {[...items, ...items].map((item, i) => (
          <li key={`${item.name}-${i}`} aria-hidden={i >= items.length}>
            <QuoteCard item={item} tone={tones[(offset + i) % tones.length]} />
          </li>
        ))}
      </ul>
    </div>
  );
}

export default function TestimonialsMarqueeColumns({
  testimonials = defaultTestimonials,
}: {
  testimonials?: Testimonial[];
}) {
  const columns = [0, 1, 2].map((c) => testimonials.filter((_, i) => i % 3 === c));

  return (
    <section className="overflow-hidden bg-background px-6 py-20">
      <style>{`
        @keyframes pp-testimonials-marquee-columns-scroll {
          from { transform: translate3d(0, 0, 0); }
          to { transform: translate3d(0, -50%, 0); }
        }
        .pp-testimonials-marquee-columns-track {
          animation: pp-testimonials-marquee-columns-scroll var(--pp-d) linear infinite;
          animation-direction: var(--pp-dir);
        }
        .pp-testimonials-marquee-columns-col:hover .pp-testimonials-marquee-columns-track {
          animation-play-state: paused;
        }
        @media (prefers-reduced-motion: reduce) {
          .pp-testimonials-marquee-columns-track { animation: none; }
        }
      `}</style>

      <div className="mx-auto grid max-w-7xl items-center gap-12 lg:grid-cols-[minmax(0,5fr)_minmax(0,9fr)]">
        <div>
          <p className="inline-flex items-center gap-2 rounded-full border border-border bg-card px-3 py-1 text-xs font-medium text-muted-foreground">
            <span className="relative flex size-2">
              <span className="absolute inset-0 animate-ping rounded-full bg-emerald-500/60 motion-reduce:animate-none" />
              <span className="relative size-2 rounded-full bg-emerald-500" />
            </span>
            2,140 teams shipped with Cadence this week
          </p>
          <h2 className="mt-6 text-4xl font-semibold tracking-tight text-balance text-foreground sm:text-5xl">
            The release tool engineers{" "}
            <span className="font-serif font-normal text-primary italic">recommend</span> to each other
          </h2>
          <p className="mt-5 max-w-md text-lg text-pretty text-muted-foreground">
            Nobody asked these teams to write anything. They posted it in our community after their first
            calm release day.
          </p>

          <div className="mt-8 flex items-center gap-4">
            <div className="flex -space-x-2">
              {testimonials.slice(0, 5).map((t, i) => (
                <span
                  key={t.name}
                  aria-hidden
                  className={cn(
                    "flex size-9 items-center justify-center rounded-full bg-linear-to-br text-[0.65rem] font-semibold text-white ring-2 ring-background",
                    tones[i % tones.length],
                  )}
                >
                  {initials(t.name)}
                </span>
              ))}
            </div>
            <div>
              <div className="flex items-center gap-0.5 text-amber-500" role="img" aria-label="4.9 out of 5 stars">
                {[0, 1, 2, 3, 4].map((s) => (
                  <Star key={s} className="size-4 fill-current" />
                ))}
              </div>
              <p className="mt-1 text-sm text-muted-foreground">
                <span className="font-medium text-foreground">4.9</span> from 612 reviews
              </p>
            </div>
          </div>

          <a
            href="#"
            className="group mt-9 inline-flex h-11 items-center gap-2 rounded-full bg-primary px-6 text-sm font-medium text-primary-foreground shadow-lg shadow-primary/25 transition-transform hover:-translate-y-0.5"
          >
            Read customer stories
            <ArrowRight className="size-4 transition-transform group-hover:translate-x-0.5" />
          </a>
        </div>

        <div className="grid h-[640px] grid-cols-1 gap-4 [mask-image:linear-gradient(to_bottom,transparent,black_14%,black_86%,transparent)] sm:grid-cols-2 xl:grid-cols-3">
          <MarqueeColumn items={columns[0]} offset={0} duration={38} />
          <MarqueeColumn items={columns[1]} offset={2} duration={46} reverse className="hidden sm:block" />
          <MarqueeColumn items={columns[2]} offset={4} duration={41} className="hidden xl:block" />
        </div>
      </div>
    </section>
  );
}
```

## AI prompt

```text
Add the "Testimonials Marquee Columns" component from PixelPapa UI to this project.

What it is: Three columns of customer quotes drift vertically in opposite directions beside a fixed headline; hovering a column pauses it.
Category: Testimonials. Depends on: lucide-react.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/testimonials-marquee-columns.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/testimonials-marquee-columns.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/testimonials-marquee-columns" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More testimonials

- [Testimonials Before After Metrics](https://pixelpapa.com/components/testimonials/testimonials-before-after-metrics.md) (Pro): A before / after switch that rewinds every card at once: the number rolls back, the bar slides to its old mark and the quote swaps from complaint to praise.
- [Testimonials Carousel Spotlight](https://pixelpapa.com/components/testimonials/testimonials-carousel-spotlight.md) (Pro): A looping carousel where the centre quote is sharp and lifted while its neighbours shrink, blur and dim; drag, use arrows or pick an avatar.
- [Testimonials Case Study Tabs](https://pixelpapa.com/components/testimonials/testimonials-case-study-tabs.md) (Pro): Company tabs with a sliding pill; each one swaps in a headline metric that counts up, a sparkline that draws itself and the quote behind the number.
- [Testimonials Featured Quote](https://pixelpapa.com/components/testimonials/testimonials-featured-quote.md) (Pro): One oversized serif quote whose words blur in, with an author switcher underneath that fills a progress line and advances on its own.
- [Testimonials Masonry Wall](https://pixelpapa.com/components/testimonials/testimonials-masonry-wall.md): A masonry wall mixing short quotes, a highlighted story and stat tiles; it fades out at the fold and unrolls to full height on demand.
- [Testimonials Rating Summary](https://pixelpapa.com/components/testimonials/testimonials-rating-summary.md) (Pro): An average score that counts up beside star-breakdown bars; click a bar to filter the review list, which reflows with layout animation.
