Components
Rating
Rating
Interactive star rating component with hover effects and customizable styling.
Installation
1
Install the packages
npm i motion clsx tailwind-merge2
Add util file
lib/util.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
3
Copy and paste the following code into your project
rating.tsx
"use client";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
import React, { useState } from "react";
import { FiStar } from "react-icons/fi";
interface RatingProps {
value?: number;
onChange?: (value: number) => void;
max?: number;
size?: "sm" | "md" | "lg";
readonly?: boolean;
showValue?: boolean;
}
const Rating: React.FC<RatingProps> = ({
value = 0,
onChange = () => {},
max = 5,
size = "md",
readonly = false,
showValue = false
}) => {
const [hoverValue, setHoverValue] = useState(0);
const sizeClasses = {
sm: "h-4 w-4",
md: "h-5 w-5",
lg: "h-6 w-6",
};
const handleClick = (rating: number) => {
if (!readonly) {
onChange(rating);
}
};
const handleMouseEnter = (rating: number) => {
if (!readonly) {
setHoverValue(rating);
}
};
const handleMouseLeave = () => {
if (!readonly) {
setHoverValue(0);
}
};
const displayValue = hoverValue || value;
return (
<div className="flex flex-col items-center gap-4">
<div className="flex items-center gap-1">
{Array.from({ length: max }).map((_, index) => {
const rating = index + 1;
const isFilled = rating <= displayValue;
return (
<motion.button
key={index}
onClick={() => handleClick(rating)}
onMouseEnter={() => handleMouseEnter(rating)}
onMouseLeave={handleMouseLeave}
className={cn(
"transition-colors",
readonly ? "cursor-default" : "cursor-pointer hover:scale-110"
)}
whileHover={!readonly ? { scale: 1.1 } : {}}
whileTap={!readonly ? { scale: 0.9 } : {}}
>
<FiStar
className={cn(
sizeClasses[size],
isFilled
? "fill-yellow-400 text-yellow-400"
: "text-neutral-400 hover:text-yellow-400"
)}
/>
</motion.button>
);
})}
</div>
{showValue && (
<motion.div
className="text-sm text-neutral-400"
key={displayValue}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
>
{displayValue} out of {max} stars
</motion.div>
)}
</div>
);
};
export default Rating;"use client";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
import React, { useState } from "react";
import { FiStar } from "react-icons/fi";
interface RatingProps {
value?: number;
onChange?: (value: number) => void;
max?: number;
size?: "sm" | "md" | "lg";
readonly?: boolean;
showValue?: boolean;
}
const Rating: React.FC<RatingProps> = ({
value = 0,
onChange = () => {},
max = 5,
size = "md",
readonly = false,
showValue = false
}) => {
const [hoverValue, setHoverValue] = useState(0);
const sizeClasses = {
sm: "h-4 w-4",
md: "h-5 w-5",
lg: "h-6 w-6",
};
const handleClick = (rating: number) => {
if (!readonly) {
onChange(rating);
}
};
const handleMouseEnter = (rating: number) => {
if (!readonly) {
setHoverValue(rating);
}
};
const handleMouseLeave = () => {
if (!readonly) {
setHoverValue(0);
}
};
const displayValue = hoverValue || value;
return (
<div className="flex flex-col items-center gap-4">
<div className="flex items-center gap-1">
{Array.from({ length: max }).map((_, index) => {
const rating = index + 1;
const isFilled = rating <= displayValue;
return (
<motion.button
key={index}
onClick={() => handleClick(rating)}
onMouseEnter={() => handleMouseEnter(rating)}
onMouseLeave={handleMouseLeave}
className={cn(
"transition-colors",
readonly ? "cursor-default" : "cursor-pointer hover:scale-110"
)}
whileHover={!readonly ? { scale: 1.1 } : {}}
whileTap={!readonly ? { scale: 0.9 } : {}}
>
<FiStar
className={cn(
sizeClasses[size],
isFilled
? "fill-yellow-400 text-yellow-400"
: "text-neutral-400 hover:text-yellow-400"
)}
/>
</motion.button>
);
})}
</div>
{showValue && (
<motion.div
className="text-sm text-neutral-400"
key={displayValue}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
>
{displayValue} out of {max} stars
</motion.div>
)}
</div>
);
};
export default Rating;4
Update the import paths to match your project setup
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | 0 | Current rating value. |
| onChange | function | () => {} | Callback when rating changes. |
| max | number | 5 | Maximum rating value. |
| size | string | md | Size of the stars (sm, md, lg). |
| readonly | boolean | false | Make the rating read-only. |
| showValue | boolean | false | Show the rating value text. |