Marquee Quote Columns
A free testimonial marquee component for React: quote cards drift up and down in offset vertical columns, pausing the moment you hover or focus.
- Automated axe check
- Reduced-motion tested
- SSR build verified
- CSS and React
- No runtime packages
Marquee Quote Columns is a free testimonial section for React and Next.js, built with Tailwind and plain CSS keyframes. You pass an array of quotes and it distributes them round-robin into two or three vertical columns, each looping seamlessly as a marquee: the middle column drifts down while its neighbours drift up, each at a slightly different speed derived from one speed prop, so the wall feels alive rather than mechanical. The top and bottom edges fade out through a mask-image gradient instead of clipping hard.
The animation is a single transform-only keyframe per column, so there is no JavaScript animation loop at all: a small effect measures each column to convert speed from pixels per second into a duration, and flips a data attribute to pause the wall while it is offscreen or the tab is hidden. Hovering anywhere in the section or moving keyboard focus inside it pauses every column in place via animation-play-state. Each quote is a real figure with a blockquote and figcaption, the loop duplicates are aria-hidden so screen readers hear every quote exactly once, and with prefers-reduced-motion enabled the marquee disappears entirely in favour of a static responsive grid.
We swapped our old testimonial slider for this wall and dwell time on the landing page doubled. It just quietly keeps moving until you reach for it, then it gets out of the way.
Accessibility review passed first try. Real figures, real blockquotes, duplicates hidden from the screen reader, and the whole thing pauses the moment focus lands inside it.
The edge fade masks make it look like the quotes surface out of the page. Clients keep asking which agency built our testimonials section.
Install command, nine quotes, done before my coffee cooled.
Feels premium without a single dependency.
Dropped it into a Next.js app router page as-is. No hydration warnings, no layout shift, and the columns pause when the tab is hidden so it never burns battery in the background.
The offset column speeds are the detail nobody notices consciously but everyone feels. Our wall finally looks alive instead of like three synchronised elevators.
I tested it with reduced motion switched on expecting the usual empty hole where the marquee used to be. Instead I got a tidy static grid with every quote readable. That sold me.
Nine quotes in, zero JavaScript animation libraries. My kind of marquee.
Install
Add the @te registry to your components.json once:
{
"registries": {
"@te": "https://templateempire.io/r/{name}.json"
}
}Then install the component:
npx shadcn@latest add @te/quote-marquee-columnsUsage
import { QuoteMarqueeColumns } from "@/components/quote-marquee-columns";
const quotes = [
{
text: "We swapped our old slider for this wall and dwell time doubled.",
name: "Maya Lindqvist",
role: "Head of Growth, Fieldnote",
},
{
text: "Install command, nine quotes, done before my coffee cooled.",
name: "Tom Okafor",
role: "Indie founder",
},
// ...more quotes
];
export default function Page() {
return <QuoteMarqueeColumns quotes={quotes} columns={3} speed={40} />;
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| quotes | { text: string; name: string; role?: string }[] | required | Testimonial quotes, distributed round-robin across the columns in order. |
| columns | 2 | 3 | 3 | Column count at the lg breakpoint. Below lg the wall stacks to two columns, then one. |
| speed | number | 40 | Base drift speed in pixels per second. Columns vary around it by up to 25% and alternate direction so they never move in lockstep. |
| className | string | undefined | Additional classes merged onto the section. |
Customisation
Marquee Quote Columns exposes three Tier 3 tokens, each consumed with a fallback so overriding them from :root or a wrapper is optional: --qmc-height sets the visible height of each column (default 30rem), --qmc-gap sets the gap between cards and columns (default 1.25rem), and --qmc-fade sets how far the top and bottom mask fades reach into each column (default 12%). The cards themselves consume the global semantic tokens (border-border, bg-surface, text-muted), so they follow your theme automatically. The className prop is merged onto the section:
:root {
--qmc-height: 36rem;
--qmc-fade: 18%;
}
<QuoteMarqueeColumns quotes={quotes} className="my-24" />Frequently asked questions
- Is Marquee Quote Columns free for commercial use?
- Yes. Marquee Quote Columns is MIT licensed, so you can use it in personal and commercial projects without attribution.
- Is Marquee Quote Columns accessible for screen reader and keyboard users?
- Yes. Every quote is a real figure containing a blockquote and a figcaption with the name and role, and the duplicated stacks that make the loop seamless are aria-hidden, so assistive technology receives each quote exactly once. The wall pauses while the pointer hovers it and while keyboard focus is anywhere inside it, which satisfies the WCAG pause requirement for moving content, and the section itself is focusable so keyboard users can reach that pause. With prefers-reduced-motion enabled there is no marquee at all: the quotes render as a static responsive grid with nothing clipped or hidden.
- How does the marquee perform with many quotes?
- Each column runs one CSS keyframe that animates only transform, so the browser composites the whole wall on the GPU and JavaScript does no per-frame work. The only scripting is a one-off measurement that converts the speed prop from pixels per second into a per-column duration, re-run by a ResizeObserver when the layout changes, plus an IntersectionObserver that pauses the animation while the section is offscreen or the tab is hidden. There are no animation libraries and no dependencies beyond the shared class utility.