Back to Machine Coding
Machine Coding
easy
mid

How would you build a star rating component in React?

State = the rating value; a hover state for preview. Render N stars, fill based on hover ?? value. Support half-stars, read-only mode, keyboard (arrows), and accessibility (radiogroup or a slider role). The trap: use real buttons/inputs, not divs, and make hover preview not clobber the committed value.

3 min read·~15 min to think through

A star rating is small but tests hover-vs-committed state separation, accessibility, and using semantic elements.

Core implementation

jsx
function StarRating({ value = 0, onChange, max = 5, readOnly = false }) {
  const [hover, setHover] = useState(null);
  const display = hover ?? value;          // hover previews, value is committed

  return (
    <div role="radiogroup" aria-label="Rating">
      {Array.from({ length: max }, (_, i) => {
        const starValue = i + 1;
        return (
          <button
            key={starValue}
            type="button"
            role="radio"
            aria-checked={starValue === value}
            aria-label={`${starValue} star${starValue > 1 ? "s" : ""}`}
            disabled={readOnly}
            onClick={() => onChange?.(starValue)}
            onMouseEnter={() => !readOnly && setHover(starValue)}
            onMouseLeave={() => !readOnly && setHover(null)}
          >
            {starValue <= display ? "★" : "☆"}
          </button>
        );
      })}
    </div>
  );
}

The key design points

  • Hover ≠ value. Keep a separate hover state; display = hover ?? value. Hovering previews a rating; clicking commits it. Conflating them means the rating changes just by mousing over — a classic bug.
  • Real interactive elements<button>s (or radio <input>s), not <div onClick>. You get keyboard, focus, and semantics for free.
  • Accessibility — it's a single choice from N: role="radiogroup" + role="radio" with aria-checked, or a role="slider" with aria-valuenow/min/max. Each star labeled ("3 stars").
  • Keyboard support — arrow keys to increase/decrease the rating, Enter/Space to commit.
  • Read-only mode — for displaying ratings (no hover, no click, not focusable as inputs).
  • Half-stars — if required: compute fill per star from a fractional value (e.g. clip-path or two overlaid layers); hover then snaps to halves based on cursor x-position within the star.
  • Controlled vs uncontrolled — support value/onChange and a defaultValue.

The framing

"State is the committed value plus a separate hover state — display = hover ?? value — so hovering previews and clicking commits; merging them means the rating changes on mouseover, which is the classic bug. I render real <button>s, not divs, so keyboard and focus come free, and wire it up as a radiogroup of radios with aria-checked and per-star labels, plus arrow-key support. A readOnly mode handles the display case. Half-stars, if needed, are a fractional fill per star with hover snapping to halves by cursor position."

Follow-up questions

  • Why keep hover state separate from the committed value?
  • Why use buttons instead of divs?
  • How do you make a star rating accessible?
  • How would you implement half-star ratings?

Common mistakes

  • Conflating hover and value — rating changes on mouseover.
  • divs with onClick — no keyboard/focus/semantics.
  • No ARIA roles, no keyboard support.
  • No read-only mode for displaying ratings.
  • Forgetting to reset hover on mouse leave.

Performance considerations

  • Trivial — N is small. The only concern is not re-rendering excessively on hover; local hover state keeps it contained.

Edge cases

  • Read-only display vs interactive input.
  • Half-star / fractional values.
  • Clearing a rating back to zero.
  • Keyboard-only users.
  • RTL layouts.

Real-world examples

  • Product reviews, feedback forms, app store ratings.
  • Read-only star displays on listing cards.

Senior engineer discussion

Seniors separate hover-preview from committed value, use semantic interactive elements, implement the radiogroup ARIA pattern with keyboard support, support read-only and controlled/uncontrolled modes, and handle half-stars.

Related questions