Skip to content
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
5 ★
74%
4 ★
18%
3 ★
5%
2 ★
2%
1 ★
1%
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.