# Free Testimonial Wall Builder

> Build a testimonial wall for free: masonry, marquee, grid or carousel. Paste X posts, CSV or JSON, then copy the React code, a PNG or a share link.

Canonical: https://hextaui.com/tools/testimonial-wall
Last updated: 2026-10-28

Turn the nice things people say about your product into a wall of quotes for your site. Paste X posts, CSV or JSON, pick a layout, frame it for a PNG to post, then copy the React code. It renders with HextaUI components and needs no account, script or subscription.

Open the builder: https://hextaui.com/tools/testimonial-wall

## What a testimonial wall is

A testimonial wall is a page section that shows many short quotes from customers at once, usually as a masonry of cards or a scrolling marquee. It works as social proof: a visitor who sees dozens of real people vouching for a product trusts it faster than one polished headline.

Hosted tools such as Senja and Testimonial.to collect testimonials for you and embed the wall with a script and a monthly plan. This builder does the other half for free. You keep the quotes as data in your own repository and the wall is plain React you can edit, with no third-party script slowing the page down.

## Layouts

- Masonry
- Grid
- Marquee
- Carousel
- Featured

## How it works

1. **Add your quotes.** Open Quotes to type them in, paste X post links, or paste JSON or CSV with name and body columns. Click any card on the wall to edit that quote.
2. **Pick a layout and a style.** Choose masonry, grid, marquee, carousel or a single featured quote, then set the card style, corners, columns, accent and which details each card shows.
3. **Frame it for a post.** Put the wall on a background with padding and an optional heading, then download a PNG to share on X or in a launch post.
4. **Export the code.** Run the install command to add the HextaUI components the wall uses, then save the file in your project and render <TestimonialWall /> where you want it.

```bash
npx shadcn@latest add https://hextaui.com/r/avatar.json https://hextaui.com/r/card.json
```

A generated file with two testimonials looks like this:

```tsx title="components/testimonial-wall.tsx"
import type { CSSProperties } from "react"
import {
  IconBrandLinkedin,
  IconBrandProducthunt,
  IconBrandX,
  IconStarFilled,
  IconWorld,
} from "@tabler/icons-react"

import {
  Avatar,
  AvatarFallback,
  AvatarImage,
  getInitials,
} from "@/components/ui/avatar"
import { Card, CardContent } from "@/components/ui/card"

type Testimonial = {
  name: string
  handle?: string
  role?: string
  avatarUrl?: string
  body: string
  source?: "x" | "linkedin" | "producthunt" | "web"
  url?: string
  date?: string
  rating?: number
}

const testimonials: Testimonial[] = [
  {
    name: "Juniper Vale",
    handle: "junipervale_demo",
    body: "Swapped our hand-rolled pricing page for this in an afternoon. Conversions are up and I finally stopped fighting CSS.",
    source: "x",
    date: "2026-09-14",
    rating: 5,
  },
  {
    name: "Theo Brightwater",
    role: "Founder, Lumenfold",
    body: "The onboarding flow is the first one our users actually finish. Support tickets about setup dropped to almost nothing within a month, and the team that used to answer them now ships features instead.",
    source: "linkedin",
    date: "2026-08-30",
    rating: 5,
  },
]

const sources = {
  x: { label: "X", icon: IconBrandX },
  linkedin: { label: "LinkedIn", icon: IconBrandLinkedin },
  producthunt: { label: "Product Hunt", icon: IconBrandProducthunt },
  web: { label: "Website", icon: IconWorld },
}

function Rating({ value }: { value: number }) {
  return (
    <div
      role="img"
      aria-label={`Rated ${value} out of 5`}
      className="flex items-center gap-0.5"
    >
      {[1, 2, 3, 4, 5].map((star) => (
        <IconStarFilled
          key={star}
          aria-hidden
          className={
            star <= value
              ? "size-3.5 text-warning"
              : "size-3.5 text-foreground/15"
          }
        />
      ))}
    </div>
  )
}

function Source({ testimonial }: { testimonial: Testimonial }) {
  if (!testimonial.source) return null
  const { label, icon: Icon } = sources[testimonial.source]
  if (!testimonial.url) {
    return (
      <Icon
        role="img"
        aria-label={label}
        className="size-4 shrink-0 self-start text-muted-foreground"
      />
    )
  }
  return (
    <a
      href={testimonial.url}
      target="_blank"
      rel="noopener noreferrer"
      aria-label={`View on ${label}`}
      className="-m-1 shrink-0 self-start rounded-md p-1 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden motion-reduce:transition-none"
    >
      <Icon aria-hidden className="size-4" />
    </a>
  )
}

function TestimonialAvatar({
  testimonial,
  size,
}: {
  testimonial: Testimonial
  size?: "lg"
}) {
  return (
    <Avatar size={size}>
      {testimonial.avatarUrl ? (
        <AvatarImage
          src={testimonial.avatarUrl}
          alt=""
          referrerPolicy="no-referrer"
        />
      ) : null}
      <AvatarFallback>{getInitials(testimonial.name)}</AvatarFallback>
    </Avatar>
  )
}

function TestimonialCard({ testimonial }: { testimonial: Testimonial }) {
  const subtitle = testimonial.handle
    ? `@${testimonial.handle}`
    : testimonial.role

  return (
    <Card variant="default" size="sm" className="h-full">
      <CardContent render={<figure />}>
        <figcaption className="flex items-center gap-3">
          <TestimonialAvatar testimonial={testimonial} />
          <div className="min-w-0 flex-1">
            <p className="truncate text-sm font-medium text-foreground">{testimonial.name}</p>
            {subtitle ? <p className="truncate text-sm text-muted-foreground">{subtitle}</p> : null}
          </div>
          <Source testimonial={testimonial} />
        </figcaption>
        <blockquote className="text-sm/6 text-pretty wrap-anywhere whitespace-pre-line text-foreground">{testimonial.body}</blockquote>
        {testimonial.rating ? (
          <div className="mt-auto flex items-center justify-between gap-3">
            {testimonial.rating ? (
              <Rating value={testimonial.rating} />
            ) : null}
          </div>
        ) : null}
      </CardContent>
    </Card>
  )
}

export function TestimonialWall() {
  return (
    <section
      aria-label="Testimonials"
      data-slot="testimonial-wall"
      data-layout="masonry"
      className="@container w-full"
      style={{ "--radius": "0.8rem" } as CSSProperties}
    >
      <div className="columns-1 gap-4 *:mb-4 *:break-inside-avoid @lg:columns-2 @3xl:columns-3">
        {testimonials.map((testimonial, index) => (
          <TestimonialCard key={index} testimonial={testimonial} />
        ))}
      </div>
    </section>
  )
}
```

## Questions

### Is the testimonial wall builder free?

Yes. There’s no account, no watermark and no limit on exports. The code it gives you uses free, MIT-licensed HextaUI components, so you can ship it in any project, including commercial ones.

### How is this different from Senja or Testimonial.to?

Those services collect testimonials through forms and host the wall for you behind a monthly plan and an embed script. This builder doesn’t collect anything. It turns quotes you already have into a React component you own, with no script, no hosting and no subscription.

### Can I import posts from X?

Yes. Paste one or more x.com or twitter.com post links and the builder reads each public post’s author, text and date through X’s oEmbed endpoint. Posts from protected accounts can’t be read, so add those by hand. For LinkedIn and Product Hunt, paste the text into the form.

### What format does bulk import accept?

A JSON array of objects or CSV with a header row. Each testimonial needs a name and a body, which can also be called author, text or quote. Handle, role, avatar, source, url, date and rating are optional. Tab-separated rows pasted from a spreadsheet work too.

### What does the exported code need?

A React project with Tailwind CSS v4 and the shadcn CLI. The install command adds the HextaUI Card and Avatar components, plus Carousel for the carousel layout. The file inlines your testimonials, so there’s nothing to fetch at runtime.

### Where are my testimonials stored?

Only in your browser. A share link stores the whole wall, compressed, in the part of the URL after the #, which browsers never send to a server. Importing an X post sends only that post’s link.

### Can I share the wall as an image?

Yes. Frame puts the wall on a background with padding and an optional heading, and PNG downloads exactly what the canvas shows at twice the resolution, in light or dark mode. The frame is only for the image; the exported code is the wall itself.

### Does the wall support dark mode and reduced motion?

Yes. Cards use your theme’s tokens, so they follow light and dark mode. The marquee pauses on hover and keyboard focus, and it stops and becomes a scrollable row for people who prefer reduced motion.

## Related

- [All tools](https://hextaui.com/tools)
- [Components](https://hextaui.com/components)
- [Blocks](https://hextaui.com/blocks)
