Testimonial wall
A masonry wall of short quotes in balanced columns, one to three wide.
npx shadcn@latest add https://ui.goodtabs.co/r/testimonial-wall.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/testimonial-wall
Three components in, and nothing needed restyling.
My agent installed the table and the command menu with one prompt each. The tokens just matched our theme, which is not something I expected to say.
Calm, dense and fast. Our dashboard finally looks like one product.
The focus states are right out of the box.
Four quotes of different lengths in two columns.
Three columns
Three components in, and nothing needed restyling.
My agent installed the table and the command menu with one prompt each. The tokens matched our theme.
Calm, dense and fast.
The focus states are right out of the box.
We replaced a pile of one-off cards with the registry versions in a week and the diff was mostly deletions.
Readable source, no surprises.
Six quotes in three columns from the md breakpoint.
Usage
import { TestimonialWall } from "@/components/testimonial-wall"
<TestimonialWall quotes={[{ quote: "Calm, dense and fast.", name: "Ines Calloway", role: "Founder" }]} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| quotes | { quote: string; name: string; role?: string }[] | Short quotes. Lengths can differ. | |
| columns | 2 | 3 | 3 | Columns from md up. One column on phones, two from sm. |
Source
testimonial-wall/testimonial-wall.tsx
import * as React from "react";
import { cn } from "@/lib/utils";
export type WallQuote = {
/** The quote, as plain text. */
quote: string;
/** Who said it. */
name: string;
/** Their role, one short line. */
role?: string;
};
type TestimonialWallProps = React.ComponentProps<"div"> & {
/** Short quotes. Lengths can differ, the columns balance themselves. */
quotes: WallQuote[];
/** Columns from the md breakpoint up. One column on phones, two from sm. */
columns?: 2 | 3;
};
/**
* A masonry wall of short quotes in CSS columns, so cards of different heights pack without gaps.
* Server component. Order reads down each column, not across.
*/
export function TestimonialWall({ quotes, columns = 3, className, ...props }: TestimonialWallProps) {
return (
<div
data-slot="testimonial-wall"
className={cn("columns-1 gap-3 sm:columns-2", columns === 3 && "md:columns-3", className)}
{...props}
>
{quotes.map((q, i) => (
<figure key={i} className="m-0 mb-3 flex break-inside-avoid flex-col gap-3 rounded-lg border bg-card p-4">
<blockquote className="m-0 text-sm text-foreground">{q.quote}</blockquote>
<figcaption className="flex flex-col">
<span className="text-sm font-medium text-foreground">{q.name}</span>
{q.role && <span className="text-xs text-muted-foreground">{q.role}</span>}
</figcaption>
</figure>
))}
</div>
);
}
For AI agents
Hand your agent this page, the markdown at /components/testimonial-wall/prompt.md, or the Copy AI prompt button above.
When to use
- A page of six or more short quotes
- When quotes vary in length and a rigid grid would leave gaps
- Under a features section as proof
When not to use
- One standout quote, which belongs in testimonial-spotlight
- Quotes with avatars and sources, where testimonial-card fits
- Fewer than four quotes
Agent notes
- Server component using CSS columns; order reads down each column, not across
- Cards are figure elements with blockquote and figcaption, using bg-card and a hairline border
- Keep each quote to about two sentences or the wall gets tall
- Use real, permissioned quotes only; demos are fictional