Interactive Star Rating
A 5-star rating control with hover preview and a springy fill pop.
- Category
- Components
- Added
- 2026-08-22
- Deps
- 2
- Updated
- 2026-09-01
Props
"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>
);
}
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.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Animated Checkbox
componentsA checkbox whose checkmark draws itself in as a real SVG path animation.
Ripple Roll Button
componentsA 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.