Gemini_Generated_Image_5urnoz5urnoz5urn.png


A Star Rating Widget is one of the most commonly asked frontend system design questions at companies like Atlassian, Google, and ServiceNow etc..

At first glance, it seems simple — just some stars you can click to rate. But in reality, handling hover effects, edge cases, accessibility, and clean state management makes it a perfect interview challenge.

Why This Question Matters

Even though it looks trivial, interviewers are assessing:

  • Component architecture skills: Can you break down a UI into reusable, maintainable pieces?
  • State management: Do you know how to handle hover vs. selected state cleanly?
  • Edge case handling: What happens if a user clicks twice, or hovers and clicks?
  • Accessibility & UX: Can your component work with keyboard, screen readers, and smooth interactions?

Step 1: Understanding the Requirements

Clarify requirements with your interviewer:

  • Number of stars (default 5, but configurable)
  • Interactive vs. read-only
  • Hover previews for selection
  • Half-star support (optional)
  • Accessibility: keyboard navigation, ARIA roles
  • Multiple instances on the same page

Step 2: Thinking in the Component Composition Pattern

Instead of one monolithic component, we can structure the Star Rating as a composable system:

<Rating max={5} value={3.5} onChange={setValue}>
<Rating.Star value={1} />
<Rating.Star value={2} />
<Rating.Star value={3} />
<Rating.Star value={4} />
<Rating.Star value={5} />
</Rating>

Why this approach stands out in interviews:

  • Separation of Concerns: Rating handles all state and orchestration, while each Star is dumb and only focuses on rendering.
  • Scalable: Easy to extend — add 10 stars, half-stars, or even emojis instead of stars.
  • Accessible: Logic can remain in the Rating wrapper, while each Star is simple and clean.
  • Declarative API: Interviewers love seeing a clear, readable usage pattern — it’s obvious how the component works just by reading the JSX.

This pattern demonstrates thoughtful architecture and shows that you understand component composition, a key topic in frontend system design interviews.

Step 3: Using Context API for Clean State Management

We use Context API to share state across stars without prop drilling:

import { createContext, useContext } from "react";

const StarContext = createContext({
activeStar: 0,
hoverStar: 0,
starClickHandler: () => {},
starHoverHandler: () => {},
});

function StarContextProvider({ children, value }) {
return <StarContext.Provider value={value}>{children}</StarContext.Provider>;
}

const useStarContext = () => useContext(StarContext);

export { StarContextProvider, useStarContext };


Why Context?

  • Eliminates prop drilling
  • Single source of truth
  • Enables composable components

Step 5: Main Star Widget Component

The wrapper StarWidget manages the active and hover states:

import { useEffect, useState } from "react";
import Star from "./Star";
import { StarContextProvider } from "./starContext";

function StarWidget({ changeHandler, selectedVal, starCount = 5 }) {
const [activeStar, setActiveStar] = useState(0);
const [hoverStar, setHoverStar] = useState(0);

const starClickHandler = (indx) => {
setActiveStar(indx);
if (changeHandler) changeHandler(indx);
};

const starHoverHandler = (indx) => setHoverStar(indx);

useEffect(() => {
if (selectedVal) setActiveStar(selectedVal);
}, [selectedVal]);

return (
<StarContextProvider
value={{ activeStar, hoverStar, starClickHandler, starHoverHandler }}
>
<div className="flex gap-2" role="radiogroup" aria-label="Star rating">
{Array(starCount)
.fill(0)
.map((_, indx) => (
<Star key={indx} indx={indx + 1} />
))}
</div>
</StarContextProvider>
);
}

export default StarWidget;


Key Points:

  • Local state for hover & selected stars
  • Allows multiple independent instances
  • changeHandler notifies parent of rating changes

Step 6: Individual Star Component

Each star is dumb: it just renders based on context state:

import { useStarContext } from "./starContext";

function Star({ indx }) {
const { activeStar, hoverStar, starClickHandler, starHoverHandler } = useStarContext();

const emptyColor = "grey";
const fillColor = "#edaa10";

return (
<label
onMouseEnter={() => starHoverHandler(indx)}
onMouseLeave={() => starHoverHandler(0)}
onClick={() => starClickHandler(indx)}
className="relative cursor-pointer transition-transform duration-200 hover:scale-110"
aria-label={`Rate ${indx} star${indx > 1 ? 's' : ''}`}
>
<svg
fill={(hoverStar || activeStar) >= indx ? fillColor : emptyColor}
height={40}
viewBox="0 0 25 25"
width={40}
className="transition-colors duration-200"
>
<polygon strokeWidth="0" points="9.9,1.1 3.3,21.78 19.8,8.58 0,8.58 16.5,21.78" />
</svg>
<input
type="radio"
name="star"
value={indx}
onChange={() => starClickHandler(indx)}
className="absolute -translate-x-1/2 -translate-y-1/2 opacity-0 left-1/2 top-1/2 outline-0"
aria-hidden="true"
/>
</label>
);
}

export default Star;


Key Features:

  • Custom SVG
  • Context Integration
  • Accessibility
  • Smooth Animations
  • Visual Feedback

Step 7: Usage Example

<StarWidget changeHandler={handleRatingChange} selectedVal={0} starCount={5} />
  • Multiple independent instances are possible
  • Hover preview works
  • Smooth animations & accessibility supported

Step 8: Testing & Production Considerations

  • Unit tests: Ensure Star reacts to context correctly
  • Integration tests: Clicking a star updates the state
  • Performance: Memoize context or stars to prevent unnecessary re-renders
  • Accessibility: Use ARIA labels & keyboard navigation

Key Takeaways for Interviews

  • Always break UI into composable components
  • Think Context API to manage state efficiently
  • Plan hover, click, and accessibility behaviors early
  • Explain edge cases proactively (multiple widgets, half-stars, reset)

Final result

Screenshot 2025-10-02 at 6.53.27 PM.png

Complete solution: GitHub link