Skip to content

Interactive Star Rating

A 5-star rating control with hover preview and a springy fill pop.

Freeplayfulminimal
Category
Components
Added
2026-08-22
Deps
2
Updated
2026-09-01
Preview

Props

5
3
Requires framer-motion, lucide-react
"use client";

import { useState } from "react";
import { motion } from "framer-motion";
import { Star } from "lucide-react";

export function StarRating({ max = 5, defaultRating = 0 }: { max?: number; defaultRating?: number }) {
  const [rating, setRating] = useState(defaultRating);
  const [hovered, setHovered] = useState<number | null>(null);
  const active = hovered ?? rating;

  return (
    <div className="flex gap-1" onMouseLeave={() => setHovered(null)}>
      {Array.from({ length: max }).map((_, i) => {
        const filled = i < active;
        return (
          <motion.button
            key={i}
            type="button"
            aria-label={`Rate ${i + 1} out of ${max}`}
            onMouseEnter={() => setHovered(i + 1)}
            onClick={() => setRating(i + 1)}
            animate={{ scale: filled ? 1.15 : 1 }}
            transition={{ type: "spring", stiffness: 400, damping: 15 }}
            className="text-red-500"
          >
            <Star size={28} fill={filled ? "currentColor" : "none"} strokeWidth={1.5} />
          </motion.button>
        );
      })}
    </div>
  );
}
Notes

About this component

Rating widgets built as radio inputs or a CSS checked-sibling trick can preview a rating on hover, but they can't cleanly separate what's being previewed from what's actually committed without extra markup. This component keeps those as two separate state values, hovered and rating, and computes the displayed fill as hovered if present, otherwise rating — so moving the mouse across the row previews a value without touching the real one until a star is clicked. Reach for this over the CSS-only version specifically when that hover-preview behavior matters; for a static display of an existing rating, skip the interactivity entirely. Each star springs to a larger scale on the transition from unfilled to filled, which is what gives the fill a sense of weight rather than a flat toggle. One real gap: the component is fully uncontrolled — there's no change callback in the exported code, so wiring the committed rating into a form means adding that prop yourself.

Curator’s note

The hover-preview-without-committing behavior comes from keeping hover and the real rating in two separate state variables and computing the displayed fill as `hovered ?? rating` — collapsing them into one variable would either lose the committed rating on mouse-out or make hovering permanently change it.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

A checkbox whose checkmark draws itself in as a real SVG path animation.

checkboxformsvg
NewPro

A label that rolls letter by letter to a second copy, rippling outward from wherever your pointer came in — and settling back toward where it left.

buttonhovertext-animation

3D Flip Card

components

A card that flips on a real Y-axis rotation to reveal a back face.

cardflip3d