โญ Projects

Build a Star Rating Component โ€” Hover, Half Stars, Accessible

๐Ÿ“… Jul 5, 2026 โฑ 3 min read

Tiny component, deceptive depth: hover states, keyboard access, persistence. The accessible version uses radio buttons โ€” free keyboard support, free form integration.

The radio technique

<fieldset class="stars">
  <legend>Rate this course</legend>
  <input type="radio" name="rate" id="r5" value="5"><label for="r5">โ˜…</label>
  <input type="radio" name="rate" id="r4" value="4"><label for="r4">โ˜…</label>
  <input type="radio" name="rate" id="r3" value="3"><label for="r3">โ˜…</label>
  <input type="radio" name="rate" id="r2" value="2"><label for="r2">โ˜…</label>
  <input type="radio" name="rate" id="r1" value="1"><label for="r1">โ˜…</label>
</fieldset>
.stars { display: flex; flex-direction: row-reverse; border: 0; }  /* reversed! */
.stars input { position: absolute; opacity: 0; }
.stars label { font-size: 1.8rem; color: #3a3a4a; cursor: pointer; }

/* the trick: hover colors THIS star and all AFTER it (visually: before) */
.stars label:hover, .stars label:hover ~ label,
.stars input:checked ~ label { color: #eab308; }

The row-reverse + sibling-selector combo does hover-fill with zero JS. Radios mean arrow keys work and the value submits with the form.

Read-only display (for showing averages)

.rating-display {
  --value: 4.3;
  background: linear-gradient(90deg, #eab308 calc(var(--value) / 5 * 100%), #3a3a4a 0);
  -webkit-background-clip: text; color: transparent;
}
<div class="rating-display" style="--value: 4.3">โ˜…โ˜…โ˜…โ˜…โ˜…</div>

Gradient-clipped text = exact fractional ratings, one div. Persist choices to localStorage and you have a complete review widget.

โ† All Articles