Simple star rating with pure CSS

Published: · Reading time: 7 min

A star rating is a great way to highlight user satisfaction with a product or a service. You can implement a simple star rating using only CSS, without additional assets like images, fonts, or SVGs.

In this guide, we’ll create a:

  1. star rating widget for users to leave a review
  2. star rating to display the average rating

💡 NOTE: In this guide, we’ll focus only on the visual appearance of the star rating.

We will use special Unicode symbols a.k.a. html entities to keep it as simple and native as possible.

One of the benefits of using Unicode symbols is that they are treated as a font, which means you can apply font-related CSS properties like font-size and color.

Star rating widget

We will use a form with a set of checkboxes to allow users to leave ratings without using any JavaScript or 3rd party library. It will act as a widget with which users can interact.

<form class="star-rating">
  <input class="radio-input" type="radio" id="star5" name="star-input" value="5" />
  <label class="radio-label" class for="star5" title="5 stars">5 stars</label>

  <input class="radio-input" type="radio" id="star4" name="star-input" value="4" />
  <label class="radio-label" for="star4" title="4 stars">4 stars</label>

  <input class="radio-input" type="radio" id="star3" name="star-input" value="3" />
  <label class="radio-label" for="star3" title="3 stars">3 stars</label>

  <input class="radio-input" type="radio" id="star2" name="star-input" value="2" />
  <label class="radio-label" for="star2" title="2 stars">2 stars</label>

  <input class="radio-input" type="radio" id="star1" name="star-input" value="1" />
  <label class="radio-label" for="star1" title="1 star">1 star</label>

As you can see the order of labels and checkboxes is reversed. This is done to implement the hover effect so that all starts up until the one that is hovered is selected.

Since you cannot select previous elements in CSS we’ll use the flex-direction property to reverse the order of labels and checkboxes.

.star-rating {
  display: flex;
  flex-direction: row-reversed;
  justify-content: flex-end;

Now that labels and checkboxes are displayed in the correct order, we’ll create our stars.

To do that we’ll hide checkboxes and set a pseudo-element with a content property equal to a star symbol (★).

.radio-input {
  position: fixed;
  opacity: 0;
  pointer-events: none;

.radio-label {
  cursor: pointer;
  font-size: 0;
  color: rgba(0,0,0,0.2);
  transition: color 0.1s ease-in-out;

.radio-label:before {
  content: "★";
  display: inline-block;
  font-size: 32px;

Finally, let’s add some fancy effects so that the user can see the total number of stars on hover.

.radio-input:checked ~ .radio-label {
  color: #ffc700;
  color: gold;

.radio-label:hover ~ .radio-label {
  color: goldenrod;

.radio-input:checked + .radio-label:hover,
.radio-input:checked + .radio-label:hover ~ .radio-label,
.radio-input:checked ~ .radio-label:hover,
.radio-input:checked ~ .radio-label:hover ~ .radio-label,
.radio-label:hover ~ .radio-input:checked ~ .radio-label {
  color: darkgoldenrod;

Displaying average rating

To display the total average rating we’ll use the meter tag. As the HTML Specs says:

The meter element represents a scalar measurement within a known range, or a fractional value

This tag comes with a set of attributes that determines the semantics of its gauge. In our case, it’s the min (minimum value), max (maximum value), and value (the current value).

<meter min="0" max="5" value="4.3"></meter>

As the spec suggests:

Authors are encouraged to include a textual representation of the gauge’s state in the element’s contents, for users of user agents that do not support the meter element.

So in our case to make it more semantic and user-friendly we’ll include a short label:

Product rating: <meter class="average-rating" min="0" max="5" value="4.3" title="4.3 out of 5 stars">4.3 out of 5</meter>

Now that we’re done with HTML, let’s apply some styling to make the meter tag look like stars.

First, we’ll need to visually hide the meter tag.

.average-rating {
  position: relative;
  appearance: none;
  color: transparent;
  width: auto;
  display: inline-block;
  vertical-align: baseline;

Next to display stars we’ll use a pseudo-element with a content property equal to the 5 star symbols.

To highlight the star percentage we’ll use the text background gradient hack. That is setting the linear-gradient property, combined with the background-clip and text-fill-color properties.

To calculate the fill area percentage we need to use the following formula:

rating ÷ max rating × 100%.

.average-rating::before {
  --percent: calc(4.3/5*100%);
  content: '★★★★★';
  position: absolute;
  top: 0;
  left: 0;
  color: rgba(0,0,0,0.2);
    linear-gradient(90deg, gold var(--percent), rgba(0,0,0,0.2) var(--percent));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;


The end result with complete code can be viewed on CodePen:

See the Pen Untitled by Nikita Hlopov (@nikitahl) on CodePen.

Like this article? Share it on: