Rate this project:

The HTML includes a <form> with a rating widget, and two buttons. The rating widget includes a <fieldset>, <legend>, a <div> containing five radio buttons. Users can select one of the radio buttons to rate the project.

<form>
  <fieldset class="rating">
    <legend>Rate this project:</legend>
     <div>
       <input type="radio" name="rating" value="1" aria-label="1 star" required/>
       <input type="radio" name="rating" value="2" aria-label="2 stars"/>
       <input type="radio" name="rating" value="3" aria-label="3 stars"/>
       <input type="radio" name="rating" value="4" aria-label="4 stars"/>
       <input type="radio" name="rating" value="5" aria-label="5 stars"/>
     </div>
    </fieldset>
  <input type="reset">
  <input type="submit" value="Submit a 5-star review">
</form>

We style the rating widget using CSS. The radio buttons will be made to look like stars.

The stars are all light grey by default. When the user hovers over the stars, the potential value is displayed via dark and light starts. When a rating is made, the stars are yellow and transparent. We also use CSS to disable submission if you don't rate this project as well as it deserved to be rated.

The Project's HTML

The HTML includes a project rating widget within a <form>. The form is submittable when you rate the project correctly.

The rating <fieldset> groups the five radio buttons, displayed as stars, with the <legend> informing the user of the purpose of the widget. We've included five same-named <input type="radio"> buttons. By using the same name, the user can only select one element in the group. We did not include a <label>, so included the aria-label attribute to provides a label for screen reader users. The required attribute means the user must select one of the radio buttons or the form will not pass constraint validation and will not be submittable until a radio button is selected. (In other words, the widget is invalid until a value is selected. This means we can use the :invalid pseudo-class.)

Once a radio button in a same-named group of radio button is selected, you can't return the group to an unchecked state without JS or resetting the form. so, we include a reset button created using an <input> of type reset. We also include a submit button, which we will disable if the wrong option is selected.

Understanding the CSS functionality

This CSS-only widget is all possible thanks to various input-state pseudo-classes.

Stars and star colors

We start by hiding the radio buttons and replacing them with stars:

 /* make the current radio visually hidden */
  input[type="radio"] {
    appearance: none;
    margin: 0;
    box-shadow: none;
  }
/* replace with a star */ 
input[type=radio]::after {
  content: '\2605';
  font-size: 32px;
}

The stars are all light grey by default and darker grey on hover. The pseudo-class that does the heavy lifting in this example is :invalid, which matches any input that has an invalid value. Because of the required attribute, we can use the :invalid pseudo-class.

Since the generated stars are underpinned by radio buttons in a single set, the radio buttons are all invalid until one of them is selected, thus giving a valid value to the set. The stars are colored using:

input[type=radio]:invalid::after {
  color: #ddd;
}

In order to make the stars turn dark from the first up to whichever one is being hovered or focsused by the user, we continue to use :invalid in conjunction with interaction pseudo-classes and the adjacent-sibling combinator:

div:hover input[type=radio]:invalid::after,
div:focus-within input[type=radio]:invalid::after {
	color: #888;
}
div:hover input[type=radio]:hover ~ input[type=radio]:invalid::after,
div input[type=radio]:focus ~ input[type=radio]:invalid::after {
	color: #ddd;
}

The first rule sets all the stars to be a medium gray as long as the div is hovered or has focus within itself. The second rule sets all the stars after the star being hovered/focused back to light gray, by saying, in effect, “if an input of type=radio is hovered/focused, select all the following sibling inputs with type=radio that are invalid and then style their ::after content”.

(The previous two rules could be compressed down to a single rule using :has(), but that makes things more complicated and is a bit beyond the scope of this tutorial.)

If a star is clicked/selected, this activates the underpinning radio button and gives the set of radio buttons a valid value. Thus:

div input[type=radio]:valid {
  color: orange;
}

This sets all five stars to be orange. The stars that follow whichever was clicked/selected then need to be made hollow, which is accomplished using a rule similar to the one we saw earlier that set all the stars after the hovered/focused star to be light gray:

div input[type=radio]:checked ~ input[type=radio]:not(:checked)::after{
  color: #ccc;
  content: '\2606'; /* optional. hollow star */
}

Here we used :checked to find the radio button that was activated by the click/select action, and then used the following-sibling combinator to grab all the following radio buttons that are not checked.