Rating Badge

Editorial rating with sequentially drawn stars, review count, and a soft luminous edge.

Add to your project

Pick the way you like to build.

Let your AI assistant add it

Copy this prompt and paste it into the AI tool building your site.

# Integrate this UI component

Add this component to my project.

1. Install any missing dependencies.
2. Make sure Tailwind CSS is set up and scans the new file.
3. Prefer passing props over editing the component source — changing the component itself may break it.
4. Adjust colors, size, and layout via props or wrapper classes to match my project. You know my codebase better than this snippet does.

## Component source

```tsx
'use client';

import { motion } from 'framer-motion';

// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
//
// rating     Score from 0–5 (supports halves)
// count      Review count shown beside the score
// source     Attribution label (e.g. "Product Hunt", "G2")
// variant    visual | editorial — glass pill vs hairline rule
// compact    Hide source / tighten spacing
// className  Styles for the outer shell
//
type RatingBadgeProps = {
  rating?: number;
  count?: number;
  source?: string;
  variant?: 'visual' | 'editorial';
  compact?: boolean;
  className?: string;
};

function Star({
  fill,
  index,
}: {
  fill: number;
  index: number;
}) {
  const clip = Math.max(0, Math.min(1, fill)) * 100;

  return (
    <motion.span
      className='relative inline-block h-3.5 w-3.5'
      initial={{ opacity: 0, scale: 0.6, y: 4 }}
      animate={{ opacity: 1, scale: 1, y: 0 }}
      transition={{
        duration: 0.35,
        delay: 0.08 + index * 0.07,
        ease: [0.22, 1, 0.36, 1],
      }}
      aria-hidden
    >
      <svg viewBox='0 0 20 20' className='absolute inset-0 h-full w-full text-gray-200'>
        <path
          fill='currentColor'
          d='M10 1.5l2.35 5.1 5.55.7-4.1 3.85.1 5.55L10 14.3l-4.9 2.4.1-5.55-4.1-3.85 5.55-.7L10 1.5z'
        />
      </svg>
      <span
        className='absolute inset-0 overflow-hidden'
        style={{ width: `${clip}%` }}
      >
        <svg viewBox='0 0 20 20' className='h-3.5 w-3.5 text-amber-400'>
          <path
            fill='currentColor'
            d='M10 1.5l2.35 5.1 5.55.7-4.1 3.85.1 5.55L10 14.3l-4.9 2.4.1-5.55-4.1-3.85 5.55-.7L10 1.5z'
          />
        </svg>
      </span>
    </motion.span>
  );
}

function formatCount(n: number) {
  if (n >= 1000) return `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k`;
  return n.toLocaleString();
}

export default function RatingBadge({
  rating = 4.9,
  count = 1280,
  source = 'G2',
  variant = 'visual',
  compact = false,
  className = '',
}: RatingBadgeProps) {
  const clamped = Math.max(0, Math.min(5, rating));
  const stars = Array.from({ length: 5 }, (_, i) => clamped - i);

  const shell =
    variant === 'editorial'
      ? 'inline-flex items-center gap-3 border-y border-gray-200/80 py-2'
      : 'group inline-flex items-center gap-3 rounded-full border border-gray-200/80 bg-white/70 px-3.5 py-2 shadow-[0_1px_2px_rgba(0,0,0,0.04)] backdrop-blur-md transition-[box-shadow,border-color] duration-300 hover:border-amber-200/80 hover:shadow-[0_0_0_1px_rgba(251,191,36,0.25),0_8px_24px_rgba(0,0,0,0.06)]';

  return (
    <motion.div
      className={[shell, className].filter(Boolean).join(' ')}
      initial={{ opacity: 0, y: 6 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
    >
      <div className='flex items-center gap-1' aria-hidden>
        {stars.map((fill, i) => (
          <Star key={i} fill={fill} index={i} />
        ))}
      </div>

      <div className='flex items-baseline gap-1.5'>
        <span className='text-sm font-semibold tracking-tight text-gray-900'>
          {clamped.toFixed(1)}
        </span>
        {!compact ? (
          <span className='text-xs tracking-wide text-gray-400'>
            · {formatCount(count)} reviews
          </span>
        ) : (
          <span className='text-xs text-gray-400'>({formatCount(count)})</span>
        )}
      </div>

      {!compact && source ? (
        <span className='hidden text-[10px] font-medium uppercase tracking-[0.14em] text-gray-400 sm:inline'>
          {source}
        </span>
      ) : null}

      <span className='sr-only'>
        Rated {clamped.toFixed(1)} out of 5 from {count} reviews on {source}
      </span>
    </motion.div>
  );
}

```

Works with your AI tools

ClaudeClaude*
CodexCodex*
LovableLovable
Base44Base44
Figma MakeFigma Make
ReplitReplit*
CursorCursor*
v0v0

* Compatible with most projects that use the default React/Next.js framework.

Built With

ReactTailwind CSSFramer Motion

Props

PropTypeDescription
ratingnumberScore from 0–5 (supports halves)
countnumberReview count shown beside the score
sourcestringAttribution label (e.g. "Product Hunt", "G2")
variant'visual' | 'editorial'Glass pill or hairline rule
compactbooleanHide source and tighten spacing
classNamestringStyles for the outer shell

More Trust Components

Maya Chen
Maya Chen
James Okonkwo
James Okonkwo
Sofia Reyes
Sofia Reyes
Noah Berg
Noah Berg
+2

Joined by 2.4k builders

Layered portraits with grayscale-to-color hover, spring lift, and a glass +N counter.