Innovations

Star Rating Summary

Aggregate rating display with large average, star breakdown progress bars, and recent review snippets.

Preview

Source

tsx
"use client";

import { Star } from "lucide-react";
import { googleReviews } from "@/lib/placeholders";

const ratingBreakdown = [
  { stars: 5, pct: 94 },
  { stars: 4, pct: 4 },
  { stars: 3, pct: 1 },
  { stars: 2, pct: 1 },
  { stars: 1, pct: 0 },
];

const TOTAL_REVIEWS = 127;
const AVERAGE = 4.9;

function StarRating({ rating, size = "sm" }: { rating: number; size?: "sm" | "md" | "lg" }) {
  const cls =
    size === "lg" ? "w-6 h-6" : size === "md" ? "w-5 h-5" : "w-4 h-4";
  return (
    <div className="flex gap-0.5">
      {Array.from({ length: 5 }).map((_, i) => (
        <Star
          key={i}
          className={`${cls} ${
            i < rating ? "fill-amber-400 text-amber-400" : "fill-muted text-muted"
          }`}
        />
      ))}
    </div>
  );
}

export default function StarSummary() {
  return (
    <section className="py-20 px-4 sm:px-6 bg-background">
      <div className="max-w-3xl mx-auto">
        {/* Header */}
        <div className="text-center mb-12">
          <span className="inline-block text-xs font-semibold uppercase tracking-widest text-primary bg-primary/10 border border-primary/20 rounded-full px-4 py-1.5 mb-4">
            Reviews
          </span>
          <h2 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-foreground">
            What our clients think
          </h2>
        </div>

        {/* Summary card */}
        <div className="bg-card border border-border rounded-3xl p-8 sm:p-10 shadow-sm mb-8">
          <div className="flex flex-col sm:flex-row items-center sm:items-start gap-8 sm:gap-12">
            {/* Big number */}
            <div className="flex flex-col items-center gap-2 flex-shrink-0">
              <span className="text-7xl font-extrabold text-foreground leading-none">
                {AVERAGE}
              </span>
              <StarRating rating={5} size="lg" />
              <span className="text-sm text-muted-foreground mt-1">
                {TOTAL_REVIEWS} reviews
              </span>
            </div>

            {/* Rating bars */}
            <div className="flex-1 w-full space-y-3">
              {ratingBreakdown.map(({ stars, pct }) => (
                <div key={stars} className="flex items-center gap-3">
                  <span className="text-sm font-medium text-foreground w-4 text-right flex-shrink-0">
                    {stars}
                  </span>
                  <Star className="w-3.5 h-3.5 fill-amber-400 text-amber-400 flex-shrink-0" />
                  <div className="flex-1 bg-muted rounded-full h-2.5 overflow-hidden">
                    <div
                      className="h-full bg-amber-400 rounded-full transition-all duration-700"
                      style={{ width: `${pct}%` }}
                    />
                  </div>
                  <span className="text-sm text-muted-foreground w-8 text-right flex-shrink-0">
                    {pct}%
                  </span>
                </div>
              ))}
            </div>
          </div>
        </div>

        {/* Recent review snippets */}
        <div className="space-y-4">
          <h3 className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
            Recent reviews
          </h3>
          {googleReviews.slice(0, 3).map((review) => (
            <div
              key={review.id}
              className="bg-card border border-border rounded-2xl px-6 py-4 flex items-start gap-4 hover:shadow-sm transition-shadow"
            >
              <img
                src={review.avatar}
                alt={review.name}
                className="w-9 h-9 rounded-full object-cover flex-shrink-0 mt-0.5"
              />
              <div className="flex-1 min-w-0">
                <div className="flex items-center justify-between gap-2 flex-wrap mb-1">
                  <span className="font-semibold text-sm text-foreground">{review.name}</span>
                  <div className="flex items-center gap-2">
                    <StarRating rating={review.rating} />
                    <span className="text-xs text-muted-foreground">{review.date}</span>
                  </div>
                </div>
                <p className="text-sm text-muted-foreground leading-relaxed line-clamp-2">
                  {review.text}
                </p>
              </div>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}
Claude Code Instructions

CLI Install

npx innovations add star-summary

Where to use it

Place prominently near CTAs, pricing sections, or anywhere you need to build trust before a conversion action. In Astro: import StarSummary from '../components/innovations/reviews/star-summary'; <StarSummary client:load /> In Next.js: import StarSummary from '@/components/innovations/reviews/star-summary'; // Add above or below your pricing section Update the AVERAGE and TOTAL_REVIEWS constants at the top of the component to reflect your real numbers. The breakdown percentages can also be adjusted. Recent review snippets come from src/lib/placeholders.ts.