Accessible Star Rating Radio Input with HTML and CSS
TL;DR Just want to see the code? Check out the CodePen.
A star rating system is a common feature on many websites, allowing users to provide quick and intuitive feedback. In this blog post, we will build a star rating system using only HTML and CSS, ensuring compatibility with the <form>
element by leveraging <input type="radio">
.
HTML Structure
The foundation of our star rating system is straightforward, comprising a series of radio inputs wrapped in labels. Here’s the basic HTML structure:
<label>
<input type="radio" name="star-rating" value="1" />
<span>★</span>
</label>
<!-- (...) -->
<label>
<input type="radio" name="star-rating" value="5" />
<span>★</span>
</label>
CSS for Basic Structure
To create a clean and accessible interface, we need to style the input elements and their labels. Here’s the CSS to achieve that:
input {
position: absolute;
opacity: 0;
inset: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
label {
position: relative;
color: grey;
padding: 0 0.25rem;
transition: color 0.15s;
}
In this setup, the input is hidden but remains accessible, positioned on top of the label to facilitate user interaction.
Enhancing Accessibility
We enhance the system’s accessibility by adding focus styles to the labels when the corresponding input is focused:
label:has(input:focus-visible) {
outline-offset: 1px;
outline: black solid 2px;
}
Coloring the Stars
To visually indicate the selected rating, we need to change the color of the stars based on the user’s actions. We use the :has()
selector combined with sibling combinators to achieve this.
Color for Selected Stars
We need to color the stars before and including the selected one:
label:has(~ label > input:checked),
label:has(input:checked) {
color: gold;
}
The :has(~ label > input:...)
selector targets all labels that have a sibling label with a specific input condition. In other words, it selects all labels preceding a label with the specified input (acting as a previous sibling selector).
Color for Hover State
When a user hovers over a star, we highlight it and all previous stars:
label:has(~ label > input:hover),
label:has(input:hover) {
color: goldenrod;
}
Color for Active State
To provide immediate feedback when a star is clicked:
label:has(input:active) {
color: darkgoldenrod;
}
Testing the Star Rating Input
Do you notice that the hover state does not apply correctly?
This is because label:has(~ label > input:checked)
has a higher specificity than label:has(> input:hover)
, which caused issues when hovering over a checked input.
We can resolve this by using the :is()
selector to group multiple selectors, ensuring that the specificity of both the checked and hovered states is equalized, preventing one from unintentionally overriding the other:
label:is(:has(input:checked), :has(~ label > input:checked)) {
color: gold;
}
label:is(:has(input:hover), :has(~ label > input:hover)) {
color: goldenrod;
}
label:has(input:active) {
color: darkgoldenrod !important;
}
Final Result
Combining all the above steps, we achieve an accessible and user-friendly star rating system that meets user expectations. For a live demo, you can check it out on CodePen (and a Tailwind CSS version).
This approach ensures that our star rating system is both visually appealing and functionally robust, providing an excellent user experience with minimal HTML and CSS.