Setting a star rating on click or enter with vanilla js
On Friday, I challenged you to create a star-based rating app with JavaScript.
Over the weekend, try to write some JavaScript that detects when a user has clicked a star. Make it look different from the other stars, and add some text for visually impaired users so that they know which star is selected, too.
Today, I want to share some of the solutions provided by other readers, and walk you through how I would approach it.
What others did
View source any of these to dig into the code.
Dave Buchholz sent me this working demo. On click, he’s checking to see if a .star
was clicked, then looping through all of them and adding the .selected
class to items before and up to the selected star.
Maxx Heth sent me this approach. He applies styling on hover, too, and included an undo button.
And Paul Allen sent me this example on CodePen. He also added hover styling, and you can update your selection by clicking another star.
How I approached this
Let me start this off by saying that there are no right or wrong approaches here.
There are approaches that are better, in the sense that they use less code, are easier to maintain, or run more efficiently. But other approaches are not wrong. The way I’d approach this is highly opinionated.
Using what the browser gives you
For bonus points, I issued a challenge:
Make it also work if a keyboard-only user uses the tab key to move over to their desired rating and hits enter instead of using a mouse.
Instead of listening for when the button is clicked, I listen for submit
events. This will capture both clicks, since clicking the button submits the .rating
form, and someone hitting the enter
key after tabbing over to their desired button.
This allows you to capture both approaches with one listener.
I’ll listen for all form submissions on the document (in case there are several rating forms), and check to make sure the event.target
has the .rating
class. If it does, I’ll stop it from submitting with event.preventDefault()
. Otherwise, I’ll bail.
// Listen for form submissions
document.addEventListener('submit', function (event) {
// Only run our code on .rating forms
if (!event.target.matches('.rating')) return;
// Prevent form from submitting
event.preventDefault();
}, false);
Getting the selected star
With a click event listener, you the event.listener
is the selected star.
With a form submission, it’s the form itself. To get the clicked or entered star, we instead to need to see find the element on the page that’s current in focus (since clicking on the button also brings it into focus) with the document.activeElement
property.
I like to double check that there’s an element in focus, and if there is, I’ll use the getAttribute()
method to get the [data-star]
attribute value, which tells us the rating. Then I convert that from a string into an integer with the parseInt()
function.
// Listen for form submissions
document.addEventListener('submit', function (event) {
// Only run our code on .rating forms
if (!event.target.matches('.rating')) return;
// Prevent form from submitting
event.preventDefault();
// Get the selected star
var selected = document.activeElement;
if (!selected) return;
var selectedIndex = parseInt(selected.getAttribute('data-star'), 10);
}, false);
Highlighting our selected star
I like to rely on CSS for styling whenever possible, so I added a .star.selected
class to change the color of our selected star.
.star.selected {
color: gold;
}
I could just apply that class to the selected
element, but I also issued this challenge:
Highlight all of the stars before the selected one, too. For example, if you click the third star, stars one and two should also look different.
To make that work, we need to grab every star in our rating form and loop through them. To only get stars from the submitted form, we’ll run querySelectorAll()
on our event.target
instead of the document
.
Then, we’ll use Array.from()
to convert it from a NodeList into an array so I can use newer array methods like forEach()
on it.
// Listen for form submissions
document.addEventListener('submit', function (event) {
// Only run our code on .rating forms
if (!event.target.matches('.rating')) return;
// Prevent form from submitting
event.preventDefault();
// Get the selected star
var selected = document.activeElement;
if (!selected) return;
var selectedIndex = parseInt(selected.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(event.target.querySelectorAll('.star'));
}, false);
Looping through all of the stars
Now, I’ll use Array.forEach()
to loop through the stars, passing in arguments for both the star and its index in the array.
If the index
(which starts at 0
, not 1
) is lower than the selectedIndex
, the item is either the selected star or before it, so I’ll add the .selected
class. Otherwise, I’ll remove it.
// Listen for form submissions
document.addEventListener('submit', function (event) {
// Only run our code on .rating forms
if (!event.target.matches('.rating')) return;
// Prevent form from submitting
event.preventDefault();
// Get the selected star
var selected = document.activeElement;
if (!selected) return;
var selectedIndex = parseInt(selected.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(event.target.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);
Now, we’ve got a form that we’ll highlight the selected star with a mouse or keyboard, and lets you change your rating.
We’re almost done.
Accessibility for visually impaired users
One important part of this script is that visually impaired users need to know which star is selected, since they can’t see the highlighted color.
I had originally suggested:
add some text for visually impaired users so that they know which star is selected, too.
My friend and accessibility expert Scott O’Hara reached out to me and suggested I instead use the aria-pressed
attribute. This role is specific to buttons, and indicates whether it’s pressed or not.
To do this, I’ll first locate any previously selected star by looking for [aria-pressed="true"]
inside our event.target
. If one exists, I’ll remove the role. Then I’ll add it to our selected
element.
// Listen for form submissions
document.addEventListener('submit', function (event) {
// Only run our code on .rating forms
if (!event.target.matches('.rating')) return;
// Prevent form from submitting
event.preventDefault();
// Get the selected star
var selected = document.activeElement;
if (!selected) return;
var selectedIndex = parseInt(selected.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(event.target.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');
}
});
// Remove aria-pressed from any previously selected star
var previousRating = event.target.querySelector('.star[aria-pressed="true"]');
if (previousRating) {
previousRating.removeAttribute('aria-pressed');
}
// Add aria-pressed role to the selected button
selected.setAttribute('aria-pressed', true);
}, false);
Scott also pointed out that with our current setup, assistive technology like Voice Over will read “Black Star” on each button because of the icon we’ve used.
You can avoid this by using something like an SVG icon instead, or you can wrap the item in a span
with the aria-hidden="true"
attribute.
<button type="submit" class="star" data-star="1">
<span aria-hidden="true">★</span>
<span class="screen-reader">1 Star</span>
</button>
Now we’re officially done. The whole thing is about 40 lines of code, including whitespace and comments.
You can find the updated source code for this on GitHub.
Next challenge:
This is a great start, but most star-rating systems also adjust highlighting on hover. As in, if you hover over a star, it will highlight it to show you what the rating will be before you’ve made your selection.
I’d like to you to build off what I’ve got so far to highlight stars on hover, and also if a user tabs through the selection with their keyboard instead of using a mouse.
We’ll come back to this on Friday to give you some time to work on it. If you finish before then, send me your work! I’d love to share it with everyone.