Skip to main content Accessibility Feedback

Showing star-based ratings on hover or focus with vanilla JavaScript

Last week, we built a star-based rating system with vanilla JavaScript.

As a follow-up challenge, I asked you to highlight stars on hover and focus so that user can see what their selection would look like. I was supposed to dive into this on Friday but didn’t have time to put together a proper article, so we’re going to look at that today.

Listening for hover events

For this to work, we can’t just use a :hover selector in our CSS. We need to highlight not just the hovered on star, but the ones before it, and for that, we need JavaScript.

Let’s first setup an event listener for hover events. The event we want for this is mouseover. A hover event does not exist.

document.addEventListener('mouseover', function (event) {
	// Our code will go here...
}, false);

This is going to bubble up all hover events in the DOM, so we want to check if the event has happened in our .rating form.

We’ll use the closest() method on our event.target to first make sure the event happened on a .star, and to get the .rating form, which we’ll need later in our script.

Why closest() instead of matches()? Our stars are wrapped in a span aria-hidden="true" element, and that’s the element the event.target will map to in most cases, so we need an easy way to check if a parent element has the .star class.

document.addEventListener('mouseover', function (event) {

	// Only run our code on .rating forms
	var star = event.target.closest('.star');
	var form = event.target.closest('.rating');
	if (!star || !form) return;

}, false);

Highlight up to the hovered element

The process of highlighting up to our hovered star is pretty much the same as highlighting up to the selected one.

We’ll use getAttribute() on our star to get the hovered star index, and parseInt() to convert it to an integer. We’ll use querySelectorAll() on the form to get all stars in that .ratings form.

Finally, we’ll loop over each one with forEach() and compare it’s index against the hovered star index, adding or removing the .selected class as needed.

document.addEventListener('mouseover', function (event) {

	// Only run our code on .rating forms
	var star = event.target.closest('.star');
	var form = event.target.closest('.rating');
	if (!star || !form) return;

	// Get the selected star
	var selectedIndex = parseInt(star.getAttribute('data-star'), 10);

	// Get all stars in this form (only search in the form, not the whole document)
	// Convert them from a node list to an array
	// https://gomakethings.com/converting-a-nodelist-to-an-array-with-vanilla-javascript/
	var stars = Array.from(form.querySelectorAll('.star'));

	// Loop through each star, and add or remove the `.selected` class to toggle highlighting
	stars.forEach(function (star, index) {
		if (index < selectedIndex) {
			// Selected star or before it
			// Add highlighting
			star.classList.add('selected');
		} else {
			// After selected star
			// Remove highlight
			star.classList.remove('selected');
		}
	});

}, false);

Not bad!

Now let’s add the same feature for focus events, too.

Highlighting the focused star

The great news here is that we can use the same exact function on focus events.

To avoid writing the same code twice, let’s pull the function out of our mouseover event into a standalone function.

// Highlight the hovered or focused star
var highlight = function (event) {

	// Only run our code on .rating forms
	var star = event.target.closest('.star');
	var form = event.target.closest('.rating');
	if (!star || !form) return;

	// Get the selected star
	var selectedIndex = parseInt(star.getAttribute('data-star'), 10);

	// Get all stars in this form (only search in the form, not the whole document)
	// Convert them from a node list to an array
	// https://gomakethings.com/converting-a-nodelist-to-an-array-with-vanilla-javascript/
	var stars = Array.from(form.querySelectorAll('.star'));

	// Loop through each star, and add or remove the `.selected` class to toggle highlighting
	stars.forEach(function (star, index) {
		if (index < selectedIndex) {
			// Selected star or before it
			// Add highlighting
			star.classList.add('selected');
		} else {
			// After selected star
			// Remove highlight
			star.classList.remove('selected');
		}
	});

};

Now, we can run that function both mouseover and focus events. The event is automatically passed in as an argument.

By default, focus events don’t bubble, but if we pass in true for useCapture, the last argument in addEventListener(), we can get event bubbling for the event.

// Listen for hover and focus events on stars
document.addEventListener('mouseover', highlight, false);
document.addEventListener('focus', highlight, true);

Excellent!

You can find the full source code for this on GitHub.

One small problem

There’s one small problem, though: this removes highlighting when a star is already selected.

For Friday, try to figure out how to reset the highlighting for the selected star, if one exists, or remove all highlighting if nothing has been selected yet.