5 Stars, Would Recommend: HTML + CSS Only Star Rating Input
Be aware: This post utilizes the sibling selector and flex-direction: row-reverse
to achieve the desired star rating effect.However, this method is now outdated. With the introduction of the :has()
CSS function selector, we can more efficiently style labels based on the state of subsequent elements. This new solution also eliminates the accessibility drawbacks associated with using flex-direction: row-reverse
.
For the updated approach, please refer to the new version of the blog post:
Accessible Star Rating Radio Input with HTML and CSS
Learn to build an accessible, form-compatible star rating system using HTML and CSS, focusing on usability and visual feedback with detailed code examples.
Modern UIs frequently use star rating systems. We'll discuss a method based solely on HTML and CSS, with emphasis on <form>
element compatibility by using <input type="radio">
:
1. Constructing the HTML Architecture
To initiate our examination, let's dissect the HTML schema of our star rating system:
<div class="star-rating">
<input type="radio" id="sr-0-5" name="star-rating-0" value="5" />
<label for="sr-0-5">★</label>
...
<input type="radio" id="sr-0-1" name="star-rating-0" value="1" />
<label for="sr-0-1">★</label>
</div>
Note: The HTML schema orders the stars from a count of 5 to 1, as opposed to the normal 1 to 5 order. This inverted order is an active decision we've made, which ties directly into our CSS design choice - flex-direction: row-reverse
. Utilizing this structure, we leverage native CSS to accurately represent both selected and hover states, with optimal visual representation of the stars.
2. Establishing the CSS Framework
With our HTML structure established, we now explore its corresponding CSS:
.star-rating {
position: relative;
display: inline-flex;
flex-direction: row-reverse;
justify-content: flex-end;
margin: 0 -0.25rem;
}
The property flex-direction: row-reverse
adjusts the visual orientation of our stars, ensuring that the first star is denoted as "1" and the last is marked "5", even though they're reverse coded in our HTML.
3. Concealed Inputs and Evident Star Icons
While our radio buttons remain hidden, the stars, represented by labels, are prominently displayed:
input {
position: absolute;
opacity: 0;
}
label {
cursor: pointer;
color: grey;
padding: 0 0.25rem;
transition: color 0.15s;
}
4. Utilizing the CSS Sibling Combinator
The reversed order in the HTML, combined with flex-direction: row-reverse
, permits the effective use of the CSS sibling combinator ~
:
input:checked ~ label {
color: gold;
}
input:hover ~ label {
color: goldenrod;
transition: none;
}
This will render all stars preceding the chosen star in gold. Given our decision to invert the visual sequence of the stars, the appearance aligns precisely with expectations: all stars up to and including the selected or hovered one are displayed in gold.
5. Augmented Interaction Response
For enhanced user response, an activated star adopts a marginally darker hue:
label:active {
color: darkgoldenrod !important;
}
To enhance accessibility, stars receiving focus acquire an outline:
input:focus-visible + label {
outline-offset: 1px;
outline: #4f46e5 solid 2px;
}
Note: we've used the adjacent sibling combinator +
here instead of ~
to ensure that the outline is only applied to the label of the focused input, and not to all labels.
6. ARIA Compliance
While the flex-direction: row-reverse design choice greatly aids the visual representation of the star rating system, it does introduce an unintended consequence on keyboard navigation. Specifically, using the arrow keys for navigation may not align with user expectations due to the inverted HTML order.
To rectify this, we can employ a minimal JavaScript solution. This script listens for arrow key events within the star rating container and appropriately adjusts the focus and checked state of the radio buttons.
for (const starRating of document.getElementsByClassName("star-rating")) {
starRating.addEventListener("keydown", (e) => {
let action;
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
action = "next";
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
action = "previous";
} else {
return;
}
e.preventDefault();
const inputs = Array.from(starRating.querySelectorAll("input"));
for (let i = 0; i < inputs.length; i++) {
if (document.activeElement === inputs[i]) {
// focus the next/previous element, since we have reversed the order of the elements we need to subtract on next and add on previous
let focusToIndex = action === "next" ? i - 1 : i + 1;
if (focusToIndex < 0) focusToIndex = inputs.length - 1;
if (focusToIndex >= inputs.length) focusToIndex = 0;
inputs[focusToIndex].focus();
inputs[focusToIndex].checked = true;
break;
}
}
});
}
This JavaScript addition ensures that the star rating system remains both visually coherent and functionally intuitive for keyboard-centric users, creating a seamless user experience across the board.