Testimonial Cards with Star Ratings and Initials Avatars

A review section with three equal cards. Each card has a row of five-pointed stars, a short bold headline, the quote, and the reviewer's name and role beside an initials avatar. A summary above the cards shows the average score and review count. The cards sit in three columns on desktop and stack into one column on phones, and the author rows line up along the bottom even when quotes differ in length.

Code

const reviews = [
  {
    rating: 5,
    title: "Invoices go out the same day now",
    quote: "I used to batch my invoicing on Sunday nights. Now I send each one from my phone before I have left the client's driveway, and most are paid within the week.",
    name: "Maren Okafor",
    role: "Owner, Saltgrass Bakery",
    initials: "MO",
    avatar: "bg-amber-200 text-amber-900",
  },
  {
    rating: 5,
    title: "My accountant finally stopped emailing me",
    quote: "Everything she needs is in one export. Tax season took an afternoon this year.",
    name: "Dev Raman",
    role: "Founder, Pine and Thread Studio",
    initials: "DR",
    avatar: "bg-teal-200 text-teal-900",
  },
  {
    rating: 4,
    title: "Simple enough for the whole shop",
    quote: "Three of us share one account and nobody needed training. I would like more report layouts, but the basics are done very well and support answers within the hour.",
    name: "Lucia Brandt",
    role: "Operations lead, Northquay Cycles",
    initials: "LB",
    avatar: "bg-rose-200 text-rose-900",
  },
];

const starPath = "M10 1.6l2.47 5.3 5.8.72-4.28 3.99 1.12 5.74L10 14.5l-5.11 2.85 1.12-5.74L1.73 7.62l5.8-.72L10 1.6z";

function Stars({ rating }: { rating: number }) {
  return (
    <div className="flex gap-0.5" role="img" aria-label={`Rated ${rating} out of 5`}>
      {[1, 2, 3, 4, 5].map((n) => (
        <svg key={n} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" className={`h-5 w-5 ${n <= rating ? "text-amber-500" : "text-stone-200"}`}>
          <path d={starPath} />
        </svg>
      ))}
    </div>
  );
}

export default function TestimonialCardsWithStarRatings() {
  return (
    <section className="bg-stone-50 px-6 py-16 text-stone-900 sm:py-20">
      <div className="mx-auto max-w-6xl">
        <div className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
          <div className="max-w-xl">
            <p className="text-sm font-semibold text-amber-700">Customer reviews</p>
            <h2 className="mt-2 text-3xl font-semibold tracking-tight sm:text-4xl">Small shops run their books on Tallybird</h2>
          </div>
          <div className="flex items-center gap-4">
            <p className="text-4xl font-semibold tracking-tight">4.8</p>
            <div>
              <Stars rating={5} />
              <p className="mt-1 text-sm text-stone-600">Based on 312 reviews</p>
            </div>
          </div>
        </div>
        <ul className="mt-12 grid gap-6 md:grid-cols-3">
          {reviews.map((r) => (
            <li key={r.name} className="flex">
              <figure className="flex w-full flex-col rounded-2xl border border-stone-200 bg-white p-7">
                <Stars rating={r.rating} />
                <blockquote className="mt-5 flex-1">
                  <p className="text-lg font-semibold leading-snug">{r.title}</p>
                  <p className="mt-3 leading-relaxed text-stone-600">{r.quote}</p>
                </blockquote>
                <figcaption className="mt-7 flex items-center gap-3 border-t border-stone-100 pt-5">
                  <span aria-hidden="true" className={`flex h-11 w-11 flex-none items-center justify-center rounded-full text-sm font-semibold ${r.avatar}`}>{r.initials}</span>
                  <span className="min-w-0">
                    <span className="block text-sm font-semibold">{r.name}</span>
                    <span className="block text-sm text-stone-600">{r.role}</span>
                  </span>
                </figcaption>
              </figure>
            </li>
          ))}
        </ul>
      </div>
    </section>
  );
}

Install with one command: npx shadcn@latest add https://we.inc/r/testimonial-cards-with-star-ratings.json

When to use it

How to customize it

Accessibility

Questions

How do I show a half star?

This component rounds to whole stars to stay simple. For halves, draw the empty star first and place a filled star over it inside a wrapper with `overflow-hidden` and `w-1/2`, then keep the true number in the `aria-label`.

Can I use photos instead of initials?

Yes. Replace the initials `span` with an `img` that has the same `h-11 w-11 rounded-full` classes plus `object-cover`, and give it an empty `alt` because the name is already written next to it.

Why do the author rows line up at the bottom?

Each card is a flex column and the quote has `flex-1`, so it takes the spare height and pushes the author row down. Keep those two classes if you restyle the card.

Should I add review structured data?

Only if the reviews are genuine and about a product or service on that page. Search engines ignore or penalise review markup for testimonials a business has picked about itself, so check the current guidelines first.

Does it work in Tailwind CSS v3 and v4?

Yes. It uses core utilities only, with no plugins and no custom theme values.

Related: Large Quote Testimonial with Author and Role, Pricing Table with Monthly and Yearly Toggle, Split Hero with Email Capture, Product Card with Price, Rating, Colour Swatches and Add to Cart.

More testimonials or all free UI components. Start for free to have a full site built around it.

More in Testimonials

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap