Star Rating Input
Machine-coding brief for star rating — hover preview, keyboard, half stars optional, and radiogroup a11y pattern.
- machine-coding
- interview
- react
- a11y
Problem statement
Build a star rating input: user chooses 1…N stars (usually 5). Support controlled value, hover preview, and full keyboard use. Interviewers score the radiogroup mental model and hover vs committed value split.
Requirements
Must have
- Display
maxstars (default 5) - Controlled
value+onChange(0 = unset or 1…max) - Click sets rating
- Hover previews rating without committing
- Keyboard: arrows change value; Home/End
- Accessible name and current value
Should have
- Read-only display mode
- Clear rating control
- Labels (“3 out of 5”)
Nice to have
- Half-star precision
- Custom icons
- Animate on select
Planning (5 minutes out loud)
valuevshoverValue- Radiogroup of radio buttons or slider pattern — APG has rating as radio often
- MVP — click + hover + keyboard on group
- Don’t use only CSS checkbox hacks without a11y story
- Half stars only if time — doubles hit targets
Architecture
StarRating
├── StarButton × max
└── sr-only status
API
type StarRatingProps = {
value: number; // 0..max
onChange: (value: number) => void;
max?: number;
readOnly?: boolean;
label?: string;
id?: string;
};
Implementation sketch
function StarRating({
value,
onChange,
max = 5,
readOnly = false,
label = "Rating",
}: StarRatingProps) {
const [hover, setHover] = useState<number | null>(null);
const display = hover ?? value;
function set(n: number) {
if (readOnly) return;
onChange(n);
}
return (
<div
role="radiogroup"
aria-label={label}
className="stars"
onMouseLeave={() => setHover(null)}
onKeyDown={(e) => {
if (readOnly) return;
if (e.key === "ArrowRight" || e.key === "ArrowUp") {
e.preventDefault();
set(Math.min(max, (value || 0) + 1));
}
if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
e.preventDefault();
set(Math.max(1, value - 1 || 1));
}
if (e.key === "Home") {
e.preventDefault();
set(1);
}
if (e.key === "End") {
e.preventDefault();
set(max);
}
}}
>
{Array.from({ length: max }, (_, i) => {
const n = i + 1;
const checked = value === n;
const filled = n <= display;
return (
<button
key={n}
type="button"
role="radio"
aria-checked={checked}
aria-label={`${n} star${n > 1 ? "s" : ""}`}
tabIndex={readOnly ? -1 : value === n || (value === 0 && n === 1) ? 0 : -1}
disabled={readOnly}
className={filled ? "star filled" : "star"}
onMouseEnter={() => setHover(n)}
onFocus={() => setHover(n)}
onClick={() => set(n)}
>
<span aria-hidden>★</span>
</button>
);
})}
<span className="sr-only" aria-live="polite">
{value > 0 ? `${value} out of ${max}` : "No rating"}
</span>
</div>
);
}
Roving tabindex: when value changes, ensure the selected radio is the tab stop.
Half stars (extension)
Use a single star with two hit zones or track pointer X within star width:
function precisionFromEvent(el: HTMLElement, clientX: number, base: number) {
const rect = el.getBoundingClientRect();
const mid = rect.left + rect.width / 2;
return clientX < mid ? base - 0.5 : base;
}
Store halves as 0.5 steps; still expose clear textual value.
Accessibility essentials
| Piece | Approach |
|---|---|
| Group | role="radiogroup" + label |
| Each star | role="radio" + aria-checked |
| Keyboard | arrows per radiogroup pattern |
| Live text | “3 out of 5” |
| Read-only | present as text or aria-readonly group |
Visual fill must not be the only indicator — name includes the number.
Performance notes
- Trivial; no memo required
- Avoid heavy SVGs recreated each render without need
Footguns
- Hover sticks after mouse leave
- Click sets hover value but forgets controlled commit
- tabIndex all 0 — messy tab stops; use roving
- Using emoji alone without accessible names
- 0 stars selected with no radio checked — define initial tab stop
Interview out-loud answer
Star rating is a radiogroup: one value 1…max, hover preview is ephemeral UI state. Buttons are radios with roving tabindex and arrow key support. Read-only mode is display-only. Half-stars are pointer-X precision if product needs them. I’d keep value controlled for form libraries.
Related on this site
- Color Picker Widget
- Custom Dropdown Select
- Accessibility Interview Talking Points
- Machine Coding Interview Framework