Image depicting a waterfall under a starlit night sky, where cascading golden stars flow majestically down into a shimmering, luminescent pool below, illustrating the intricacies of the HTML and CSS-based star rating system described in the blog post.

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:


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">:

How do you like this star rating input?

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.