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.
I had put this off for eleven years, because I assumed it meant an afternoon in a solicitor’s office and four hundred pounds. It took forty minutes at the kitchen table and cost eighty. My only real regret is the eleven years.
The lasting power of attorney turned out to be the part I actually needed, and the part I did not know existed.
Twenty-two pounds a year to keep it current. Worth it twice over after the twins.
Done in one evening, and my daughter is finally off my back.
My father died in February. The grant of probate came back in five weeks and I never had to explain the situation to anybody twice.
I live in Spain half the year and own property in both countries. Ashlow asked about that on the second page. Nothing else I tried asked at all.
We have a smallholding and two children from my husband’s first marriage, which every other service I tried had no field for. Ashlow put that part in front of a solicitor and handled the rest itself.
I am a solicitor and I still used it for my own will, which I accept is an odd endorsement. The drafting is careful, the questions are the ones I would have asked, and it is honest about the cases that genuinely need a person in the room.
Everything sits in one place and my executor has her own login. When the remortgage went through I updated the house in four minutes.
Customise
Code
<QuoteMarqueeColumns quotes={quotes} />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.
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-foreground), 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.