A scalable star rating widget using CSS

This page shows how to implement a scalable star rating widget, as shown in the demonstration below. The widget scales to the surrounding text size with stars that never appear pixellated, regardless of the widget size or the visitor’s screen resolution. The widget is created purely using CSS, and does not require JavaScript.

Example rating:

Download the example code

The archives below contain example files demonstrating the styling methods described on this page.

File Size Description
example.zip 1,527 bytes Zip archive
example.7z 1,100 bytes 7-Zip archive

The HTML

The HTML for the star rating widget consists of a series of labelled radio buttons:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<span class="starRating">
  <input id="rating5" type="radio" name="rating" value="5">
  <label for="rating5">5</label>
  <input id="rating4" type="radio" name="rating" value="4">
  <label for="rating4">4</label>
  <input id="rating3" type="radio" name="rating" value="3">
  <label for="rating3">3</label>
  <input id="rating2" type="radio" name="rating" value="2">
  <label for="rating2">2</label>
  <input id="rating1" type="radio" name="rating" value="1">
  <label for="rating1">1</label>
</span>

Note that the radio buttons are ordered from highest rating to lowest; the order will be reversed by the styling.

The CSS styling

First we style the containing element to control the layout:

1
2
3
4
5
6
7
.starRating:not(old){
  display        : inline-block;
  width          : 7.5em;
  height         : 1.5em;
  overflow       : hidden;
  vertical-align : bottom;
}

The negation pseudo-class is used to hide the rule from older browsers which don’t support some of the techniques required. Visitors using these older browsers will see simple numbered radio buttons.

The container is displayed as an inline block so that it can be used inline, but with set dimensions. As there are five stars in this example, the container is given a width five times its height. Overflowing content is hidden; in conjunction with later style rules this will hide the text labels. The vertical alignment improves the positioning of the widget next to text.

Next we style the radio buttons to hide them:

1
2
3
4
.starRating:not(old) > input{
  margin-right : -100%;
  opacity      : 0;
}

The opacity renders the radio buttons invisible, while the negative margin prevents them affecting the layout.

We then style the labels to create the stars in the off state:

1
2
3
4
5
6
7
.starRating:not(old) > label{
  display         : block;
  float           : right;
  position        : relative;
  background      : url('star-off.svg');
  background-size : contain;
}

The labels are displayed as blocks and floated to the right, reversing their order so that stars representing higher ratings are shown further to the right. By positioning the labels relatively, they appear on top of the invisible radio buttons, preventing the radio buttons from being clicked. The labels are given a background image of the star in its off state; an SVG image is used so that the stars never appear pixellated, regardless of the widget size or the visitor’s screen resolution. The background is sized so that the image stretches to the width of the label.

Next we style the before pseudo-element of the labels:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.starRating:not(old) > label:before{
  content         : '';
  display         : block;
  width           : 1.5em;
  height          : 1.5em;
  background      : url('star-on.svg');
  background-size : contain;
  opacity         : 0;
  transition      : opacity 0.2s linear;
}

By using the before pseudo-element, the text within the labels is pushed down, and is then hidden due to the overflow property of the container. The pseudo-elements are given a width and height, which determines the size of the stars, and are given a background image of the star in its on state. The opacity is set so that the off state is visible by default, and the opacity is set to transition.

Finally, we add style rules to determine when the stars are shown in their on state:

1
2
3
4
5
.starRating:not(old) > label:hover:before,
.starRating:not(old) > label:hover ~ label:before,
.starRating:not(:hover) > :checked ~ label:before{
  opacity : 1;
}

The three selectors target the before pseudo-elements of the labels, changing their opacity to make each star’s on state visible. The first two selectors target a star that is being hovered over, and any following stars; these selectors let the widget show the new rating. The final selector targets the checked star, and any following stars, so long as the widget is not being hovered over; these selectors let the widget show the current rating.

Where now?

Found this useful? Share it:

Also in HTML and CSS: