Star Rating (React)
A classic star rating React component: half stars, hover-preview fill, arrow and number key control, a bounce on select, and a read-only mode that shows the average, review count and a distribution chart; icon, colour and star count are all customisable.
The most basic yet most-used rating control, done properly: every star is a button, the fill follows the pointer (in half-star mode the left half of a star is .5) and snaps back to the current value when the pointer leaves; the star you pick bounces once. Fill is built from two stacked icons — a grey base and a colour layer clipped to a percentage width — so it is not just half stars: in read-only mode an average of 4.3 fills exactly 30% of the last star. Arrow keys, Home/End and number keys drive it from the keyboard; readOnly makes it non-focusable and can pair with count to show "4.5 (1,234)" and distribution for the bar chart; icon, color and max are all swappable — pass Heart and it becomes a heart rating.
npx shadcn@latest add https://webberui.com/r/star-rating.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<StarRating />
Installation
npx shadcn@latest add https://webberui.com/r/star-rating.jsonOr, once registries are configured in components.json, install it as @webberui/star-rating.
Usage
import { StarRating, ratingSummary } from "@/components/ui/star-rating";
import { Heart } from "lucide-react";
// Interactive: half stars, click again to clear, show the readout and labels
<StarRating
allowHalf
allowClear
showValue
labels={["Poor", "Fair", "Okay", "Good", "Great"]}
onChange={(value) => console.log(value)}
/>
// Read-only average + review count + distribution bars (index 0 of distribution = 1 star)
const distribution = [12, 25, 88, 340, 812];
const { average, total } = ratingSummary(distribution);
<StarRating value={average} readOnly showValue count={total} distribution={distribution} />
// Different icon, colour and star count
<StarRating icon={Heart} color="#f43f5e" max={3} size="sm" defaultValue={2} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | — | Controlled value (0–max; may be .5 when half stars are allowed); when omitted the component is uncontrolled |
defaultValue | number | 0 | Initial value in uncontrolled mode |
onChange | (value: number) => void | — | Callback when the value changes (click, keyboard and clear all fire it) |
max | number | 5 | Number of stars |
allowHalf | boolean | false | Allow half stars: pointing at the left half of a star gives .5, and the keyboard steps by 0.5 |
readOnly | boolean | false | Read-only: not focusable, not operable, only displays the value (any decimal, e.g. an average of 4.3 fills 30% of the last star) |
disabled | boolean | false | Disabled: keeps the look but cannot be operated, rendered at reduced opacity |
size | "sm" | "md" | "lg" | number | "md" | Size: sm 16px / md 24px / lg 32px, or an explicit pixel number |
icon | LucideIcon | Star | Star icon (a lucide-react component); pass Heart for a heart rating |
color | string | "#f59e0b" | Fill colour (any CSS colour value such as #f59e0b, rgb(…), var(--primary)); amber by default |
showValue | boolean | false | Show the numeric readout next to the stars; it follows the hover preview, and shows the star's label as well when labels is provided |
count | number | — | Number of ratings, shown as "(1,234)" and included in the accessible name; usually paired with readOnly for an average score |
labels | string[] | — | Text for each star (index 0 = 1 star), e.g. ["Poor","Fair","Okay","Good","Great"]; announced as "3 星:普通" (3 stars: Okay) |
allowClear | boolean | false | Clicking the current value again clears it to 0 (Backspace / Delete / the 0 key also clear) |
distribution | number[] | — | Rating distribution: index 0 is the number of 1-star ratings, up to max stars; bars are drawn from the highest star down to the lowest |
ariaLabel | string | "評分" (rating) | Accessible name of the rating group |
className | string | — | Forwarded to the outermost container |
ratingSummary(distribution) (returns { average, total }, turning a backend distribution straight into an average and a total), formatRatingValue(value) (readout format: integers without decimals, otherwise one decimal place) and the StarRatingSize type are also named exports.
How it works
- Two stacked icons give any fill ratio: each star's base layer is the whole icon in grey; the top layer is the same icon inside a wrapper whose width is clipped to a percentage. A half star is 50%, and in read-only mode an average of 4.3 fills the fifth star to 30% — this works with any lucide icon
- Half stars are two buttons: with
allowHalf, the left and right halves of every star are separate transparent buttons (values n−0.5 and n) — no coordinate maths, and touch and mouse behave the same; in interactive mode the value is always snapped to the step so exactly one button can be marked as checked - Hover previews, never commits: the fill and readout follow whichever star the pointer is on and revert as soon as the pointer leaves the group; touch does not preview, so a tap does not flash twice
- The bounce only happens on interaction: the selected star (the whole star, for a half value) scales up and settles via keyframes; it is triggered through
useAnimationControls, so picking the same star again replays it - Keyboard: ←/↓ step down, →/↑ step up (0.5 per step with half stars), Home jumps to the lowest, End to full marks, number keys set that score directly; with
allowClear, Backspace / Delete / 0 clear. Focus follows the newly selected star (roving tabindex) readOnlyand interactive mode are exclusive: read-only renders no buttons at all, so nothing can be focused;disabledkeeps the buttons but disables them and dims the whole control
Accessibility
- Interactive mode is a
role="radiogroup"(itsaria-labelcomes fromariaLabel); every star (or every half, with half stars) is arole="radio"button,aria-checkedmarks the current value andaria-labelannounces "3 星:普通" (3 stars: Okay) or "2.5 星" (2.5 stars); only the currently checked button is in the Tab order, the rest are reached with the arrow keys - Read-only mode wraps the row in
role="img"with a label like "4.5 星,滿分 5 星,共 1,277 則評分" (4.5 stars out of 5, 1,277 ratings); the distribution bars are a semanticul, each row carries hidden text such as "5 星:812 則(64%)" (5 stars: 812 ratings, 64%), and the rating itself points at it viaaria-describedby - When disabled the buttons carry
disabledand the group is markedaria-disabled; the readout and star icons arearia-hiddenso they do not duplicate the radio announcements - When the user has "reduce motion" enabled at the system level, selecting does not bounce and hovering does not scale up — only the fill and readout change
Pattern Lock
A 3×3 connect-the-dots unlock: drag to draw the pattern, with success/failure feedback animations.
Quantity Stepper (React)
A React ± quantity stepper for ecommerce: press-and-hold acceleration, min/max with an at-limit shake, direct typing, a trash icon at zero, and rolling digits, in compact and pill variants.