Search Components...

Rating

Display a rating component.

Loading...

Installation

npx shadcn@latest add /registry/rating.json

Manual Installation

Copy and paste the following code into your project.

import React, { useId, useState } from "react";
import { Star } from "lucide-react";
import { VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { cva } from "class-variance-authority";
import { Label } from "@/components/ui/label";
 
export const ratingVariants = cva("flex gap-x-1.5", {
	variants: {
		size: {
			sm: "[&_label>svg]:size-6",
			md: "[&_label>svg]:size-8",
			lg: "[&_label>svg]:size-10",
		},
		color: {
			default: "[&_.selected-star>svg]:text-yellow-500 [&_.selected-star>svg]:fill-current",
			primary: "[&_.selected-star>svg]:text-primary [&_.selected-star>svg]:fill-primary",
			secondary: "[&_.selected-star>svg]:text-secondary [&_.selected-star>svg]:fill-secondary",
			success: "[&_.selected-star>svg]:text-success [&_.selected-star>svg]:fill-success",
			info: "[&_.selected-star>svg]:text-info [&_.selected-star>svg]:fill-info",
			warning: "[&_.selected-star>svg]:text-warning [&_.selected-star>svg]:fill-warning",
			error: "[&_.selected-star>svg]:text-error [&_.selected-star>svg]:fill-error",
		},
	},
	defaultVariants: {
		size: "md",
		color: "default",
	},
});
 
interface RatingProps extends VariantProps<typeof ratingVariants> {
	totalStars?: number;
	defaultSelected?: number;
	onChange?: (value: number) => void;
	Icon?: React.ComponentType<{ className?: string; fill?: string }>;
	className?: string;
	id?: string;
	direction?: "ltr" | "rtl";
	hoverable?: boolean;
	selectOnHover?: boolean;
}
 
const Rating = ({
	totalStars = 5,
	defaultSelected = 0,
	onChange,
	Icon = Star,
	size,
	color,
	direction = "ltr",
	className,
	id,
	hoverable = true,
	selectOnHover = false,
}: RatingProps) => {
	const [selectedStar, setSelectedStar] = useState(defaultSelected < totalStars ? defaultSelected : totalStars);
	const [hoveredStar, setHoveredStar] = useState(0);
	const generatedId = useId();
	const radioGroupId = id || generatedId;
 
	const handleStarClick = (value: number) => {
		const newValue = selectedStar === value ? 0 : value;
		setSelectedStar(newValue);
		if (onChange) {
			onChange(newValue);
		}
	};
 
	const handleStarHover = (value: number) => {
		if (hoverable) {
			setHoveredStar(value);
			if (selectOnHover) {
				setSelectedStar(value);
				if (onChange) {
					onChange(value);
				}
			}
		}
	};
 
	const handleParentMouseLeave = () => {
		if (hoverable) {
			setHoveredStar(0);
		}
	};
 
	return (
		<div
			className={cn(ratingVariants({ size, color }), className)}
			role="radiogroup"
			aria-labelledby={`${radioGroupId}-label`}
			onMouseLeave={handleParentMouseLeave}>
			{[...Array(totalStars)].map((_, index) => {
				const starValue = direction === "ltr" ? index + 1 : totalStars - index;
				const starId = `${radioGroupId}-star-${starValue}`;
 
				return (
					<div key={starValue} className="flex items-center" onMouseEnter={() => handleStarHover(starValue)}>
						<input
							type="radio"
							id={starId}
							name={radioGroupId}
							value={starValue}
							checked={selectedStar === starValue}
							onChange={() => handleStarClick(starValue)}
							className="hidden"
						/>
						<Label
							htmlFor={starId}
							className={cn(
								"cursor-pointer",
								selectedStar >= starValue || hoveredStar >= starValue ? "selected-star" : "unselected-star",
							)}
							title={`${starValue} star`}
							onClick={(e) => {
								if (selectedStar === starValue) {
									e.preventDefault();
									handleStarClick(0);
								}
							}}>
							<Icon className="transition-colors duration-300" />
						</Label>
					</div>
				);
			})}
		</div>
	);
};
 
export default Rating;