All Taste components
Data display
Rating
Star ratings to show an average with its breakdown, and to collect a new rating.
partial stars · review count · breakdown bars · rating input
4.6
128 reviews
How was Sunita's visit?Very good
"use client";
import { useState } from "react";
import { Star } from "lucide-react";
import { cn } from "@/lib/utils";
/** Read-only stars; a fractional value fills the last star partly. */
export function Stars({ value, size = 16 }: { value: number; size?: number }) {
const row = (cls: string) =>
Array.from({ length: 5 }, (_, i) => <Star key={i} className={cn("shrink-0 fill-current", cls)} style={{ width: size, height: size }} strokeWidth={0} />);
return (
<span role="img" aria-label={value + " out of 5"} className="relative inline-flex gap-0.5 leading-none text-border-strong">
{row("")}
<span className="absolute inset-y-0 left-0 inline-flex gap-0.5 overflow-hidden text-warning" style={{ width: (value / 5) * 100 + "%" }}>
{row("")}
</span>
</span>
);
}
const words = ["", "Poor", "Fair", "Good", "Very good", "Excellent"];
export function RatingInput({ label, onRate }: { label: string; onRate?: (n: number) => void }) {
const [value, setValue] = useState(0);
const [hover, setHover] = useState(0);
const shown = hover || value;
return (
<div className="grid gap-1.5">
<span className="text-[13px] font-medium">{label}</span>
<div role="radiogroup" aria-label={label} className="inline-flex gap-0.5" onMouseLeave={() => setHover(0)}>
{[1, 2, 3, 4, 5].map((n) => (
<button
key={n}
type="button"
role="radio"
aria-checked={value === n}
aria-label={n + (n === 1 ? " star" : " stars")}
onMouseEnter={() => setHover(n)}
onClick={() => { setValue(n); onRate?.(n); }}
onKeyDown={(e) => {
if (e.key === "ArrowRight" || e.key === "ArrowUp") setValue(Math.min(5, value + 1));
if (e.key === "ArrowLeft" || e.key === "ArrowDown") setValue(Math.max(1, value - 1));
}}
className={cn("grid size-8 place-items-center rounded transition hover:scale-110", n <= shown ? "text-warning" : "text-border-strong")}
>
<Star className="size-6 fill-current" strokeWidth={0} aria-hidden />
</button>
))}
</div>
<span className="text-xs text-fg-subtle">{words[shown]}</span>
</div>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.