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:
Ratinghandles all state and orchestration, while eachStaris 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
Ratingwrapper, while eachStaris 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
changeHandlernotifies 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
Starreacts 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
Complete solution: GitHub link