Skip to content
goodtabs UI
BrowseBrowse components

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.
Mira OkaforDesign engineer
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.
Tomas VerhoevenFrontend lead
Calm, dense and fast. Our dashboard finally looks like one product.
Ines CallowayFounder
The focus states are right out of the box.
Dev RanganathanAccessibility engineer

Four quotes of different lengths in two columns.

Three columns

Three components in, and nothing needed restyling.
Mira OkaforDesign engineer
My agent installed the table and the command menu with one prompt each. The tokens matched our theme.
Tomas VerhoevenFrontend lead
Calm, dense and fast.
Ines CallowayFounder
The focus states are right out of the box.
Dev RanganathanAccessibility engineer
We replaced a pile of one-off cards with the registry versions in a week and the diff was mostly deletions.
Salma HaddadStaff engineer
Readable source, no surprises.
Jonas LindqvistIndie developer

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

PropTypeDefaultDescription
quotes{ quote: string; name: string; role?: string }[]Short quotes. Lengths can differ.
columns2 | 33Columns 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