Componentform1.1 KB gzipZero dependencies
Star Rating Input
Hoverable five-star rating field with readout.
Preview
Live Preview
Rate the show
4/5
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add star-rating-inputOr install the full npm package:
npm install bigbulluiUsage
Open in StackBlitzimport { StarRatingInput } from "@/components/ui/star-rating-input";
<StarRatingInput defaultValue={4} />Usage guidance
Use Star Rating Input when a ticket stub surface needs Hoverable five-star rating field with readout. Keep props minimal and prefer composition with sibling stubs.
Avoid Star Rating Input for long form flows or dense data grids where a dedicated layout block fits better.
Source code
star-rating-input.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export interface StarRatingInputProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
value?: number;
defaultValue?: number;
onValueChange?: (value: number) => void;
max?: number;
label?: string;
}
export function StarRatingInput({
value: controlledValue,
defaultValue = 0,
onValueChange,
max = 5,
label,
className,
...props
}: StarRatingInputProps) {
const [inner, setInner] = React.useState(defaultValue);
const [hover, setHover] = React.useState(0);
const value = controlledValue ?? inner;
const shown = hover || value;
const pick = (v: number) => {
setInner(v);
onValueChange?.(v);
};
return (
<div className={cn("w-full", className)} {...props}>
{label ? (
<span className="mb-1 block font-mono text-[11px] uppercase tracking-widest text-muted-foreground">
{label}
</span>
) : null}
<div
role="radiogroup"
aria-label={label ?? "Star rating"}
className="flex items-center gap-1"
onMouseLeave={() => setHover(0)}
>
{Array.from({ length: max }).map((_, i) => {
const v = i + 1;
const lit = v <= shown;
return (
<button
key={v}
type="button"
role="radio"
aria-checked={v === value}
aria-label={`${v} star${v === 1 ? "" : "s"}`}
onMouseEnter={() => setHover(v)}
onFocus={() => setHover(v)}
onClick={() => pick(v)}
onKeyDown={(e) => {
if (e.key === "ArrowRight") pick(Math.min(max, value + 1));
if (e.key === "ArrowLeft") pick(Math.max(0, value - 1));
}}
className={cn(
"cursor-pointer p-0.5 transition-transform duration-100 hover:scale-125 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none",
lit ? "text-accent" : "text-muted-foreground/40",
)}
>
<svg width="26" height="26" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 2l2.9 6.6 7.1.6-5.4 4.7 1.6 7-6.2-3.7-6.2 3.7 1.6-7L2 9.2l7.1-.6z" />
</svg>
</button>
);
})}
<span className="ml-2 font-mono text-xs text-muted-foreground" aria-live="polite">
{shown > 0 ? `${shown}/${max}` : "Tap to rate"}
</span>
</div>
</div>
);
}
Props
| Prop | Type | Description |
|---|---|---|
| value | number | Controlled star count. |
| max | number | Star count (default 5). |
| onValueChange | (value: number) => void | Change callback. |