Rating Stars

Accessible star rating with hover preview and a playful wiggle.

How was your experience?

Installation

  1. Install the dependencies

    $ npm i motion lucide-react
  2. Copy the source into components/ui/rating-stars.tsx

    components/ui/rating-stars.tsx
    "use client";
    
    import { motion } from "motion/react";
    import { Star } from "lucide-react";
    import { useState } from "react";
    
    const labels = ["Terrible", "Bad", "Okay", "Good", "Amazing"];
    
    export function RatingStars({ value, onChange, max = 5 }: { value: number; onChange: (v: number) => void; max?: number }) {
      const [hover, setHover] = useState(0);
      const shown = hover || value;
    
      return (
        <div role="radiogroup" aria-label="Rating" className="flex gap-1" onMouseLeave={() => setHover(0)}>
          {Array.from({ length: max }, (_, i) => i + 1).map((n) => (
            <motion.button
              key={n}
              role="radio"
              aria-checked={value === n}
              aria-label={`${n} star${n > 1 ? "s" : ""}`}
              onMouseEnter={() => setHover(n)}
              onClick={() => onChange(n)}
              whileHover={{ scale: 1.2 }}
              whileTap={{ scale: 0.9 }}
              className="p-0.5"
            >
              <motion.span animate={{ rotate: value === n ? [0, -15, 15, 0] : 0 }} transition={{ duration: 0.4 }} className="block">
                <Star className={`size-8 transition-colors ${n <= shown ? "fill-amber-400 text-amber-400" : "text-zinc-300"}`} />
              </motion.span>
            </motion.button>
          ))}
        </div>
      );
    }
    
    export default function Demo() {
      const [v, setV] = useState(0);
      return (
        <div className="text-center">
          <p className="mb-3 text-sm font-medium text-zinc-900">How was your experience?</p>
          <RatingStars value={v} onChange={setV} />
          <p className="mt-3 h-5 text-sm text-zinc-500">{v ? labels[v - 1] : ""}</p>
        </div>
      );
    }
    
  3. Use it

    The file exports the component and a Demo showing typical usage. Import whichever you need.