javascript 55 lines · 7 steps

Building an accessible star-rating widget

A vanilla-JS class renders interactive stars and keeps hover, selection, and ARIA state in sync through a single paint method.

Explained by highlit
1class StarRating {
2 constructor(container, { max = 5, value = 0, onChange } = {}) {
3 this.container = container;
4 this.max = max;
5 this.value = value;
6 this.hovered = 0;
7 this.onChange = onChange;
8 this.render();
9 this.bindEvents();
10 }
11 
12 render() {
13 this.container.classList.add('star-rating');
14 this.container.innerHTML = Array.from({ length: this.max }, (_, i) =>
15 `<span class="star" data-index="${i + 1}" role="radio" tabindex="0" aria-label="${i + 1} star">\u2605</span>`
16 ).join('');
17 this.stars = [...this.container.querySelectorAll('.star')];
18 this.paint();
19 }
20 
21 bindEvents() {
22 this.stars.forEach((star) => {
23 const index = Number(star.dataset.index);
24 star.addEventListener('mouseenter', () => {
25 this.hovered = index;
26 this.paint();
27 });
28 star.addEventListener('click', () => this.select(index));
29 star.addEventListener('keydown', (e) => {
30 if (e.key === 'Enter' || e.key === ' ') {
31 e.preventDefault();
32 this.select(index);
33 }
34 });
35 });
36 this.container.addEventListener('mouseleave', () => {
37 this.hovered = 0;
38 this.paint();
39 });
40 }
41 
42 select(index) {
43 this.value = this.value === index ? index - 1 : index;
44 this.paint();
45 this.onChange?.(this.value);
46 }
47 
48 paint() {
49 const active = this.hovered || this.value;
50 this.stars.forEach((star, i) => {
51 star.classList.toggle('filled', i < active);
52 star.setAttribute('aria-checked', String(i + 1 === this.value));
53 });
54 }
55}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Routing every visual update through one paint method keeps hover and selection state consistent without scattered DOM writes.
  2. 2Building interactive DOM from a data array lets index metadata drive both rendering and event logic.
  3. 3Adding role, tabindex, and aria-checked makes a custom widget usable by keyboard and screen readers.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Building an accessible star-rating widget — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code