An illustration of a hand reaching out to touch a line of stars, with each star lighting up in sequence as if being activated by the touch. The background features a clean, modern workspace with subtle coding elements like brackets and code snippets faintly visible.

Accessible Star Rating Radio Input with HTML and CSS

How do you like this star rating input?

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

How do you like this 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.