WebberUI

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/star-rating.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

5
<StarRating />

Installation

npx shadcn@latest add https://webberui.com/r/star-rating.json

Or, 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

PropTypeDefaultDescription
valuenumberControlled value (0–max; may be .5 when half stars are allowed); when omitted the component is uncontrolled
defaultValuenumber0Initial value in uncontrolled mode
onChange(value: number) => voidCallback when the value changes (click, keyboard and clear all fire it)
maxnumber5Number of stars
allowHalfbooleanfalseAllow half stars: pointing at the left half of a star gives .5, and the keyboard steps by 0.5
readOnlybooleanfalseRead-only: not focusable, not operable, only displays the value (any decimal, e.g. an average of 4.3 fills 30% of the last star)
disabledbooleanfalseDisabled: 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
iconLucideIconStarStar icon (a lucide-react component); pass Heart for a heart rating
colorstring"#f59e0b"Fill colour (any CSS colour value such as #f59e0b, rgb(…), var(--primary)); amber by default
showValuebooleanfalseShow the numeric readout next to the stars; it follows the hover preview, and shows the star's label as well when labels is provided
countnumberNumber of ratings, shown as "(1,234)" and included in the accessible name; usually paired with readOnly for an average score
labelsstring[]Text for each star (index 0 = 1 star), e.g. ["Poor","Fair","Okay","Good","Great"]; announced as "3 星:普通" (3 stars: Okay)
allowClearbooleanfalseClicking the current value again clears it to 0 (Backspace / Delete / the 0 key also clear)
distributionnumber[]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
ariaLabelstring"評分" (rating)Accessible name of the rating group
classNamestringForwarded 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)
  • readOnly and interactive mode are exclusive: read-only renders no buttons at all, so nothing can be focused; disabled keeps the buttons but disables them and dims the whole control

Accessibility

  • Interactive mode is a role="radiogroup" (its aria-label comes from ariaLabel); every star (or every half, with half stars) is a role="radio" button, aria-checked marks the current value and aria-label announces "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 semantic ul, each row carries hidden text such as "5 星:812 則(64%)" (5 stars: 812 ratings, 64%), and the rating itself points at it via aria-describedby
  • When disabled the buttons carry disabled and the group is marked aria-disabled; the readout and star icons are aria-hidden so 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

On this page