Building Stars with HTML and CSS

Recently I became interested in making a star rating tool, which are seen on all kinds of apps and websites. There are a number of resources on this and countless ways to pull this off. Below is one way of achieving it using CSS pseudoselectors and Flexbox.

Note: While this code is Javascript-free, you’ll likely want to ultimately use JS to do something with the value that the user selects. This post explains how to build up the HTML and CSS needed to make the star rating interface look and work right for the user.

Let’s get started!

One star in the “off” state

First, let’s make just one hollow star that serves as our “off” state.

Here’s how it looks.

The HTML for the star reveals a container with the class star-rating and shows that the star is made up of two parts:

  • an input button that is either checked or not checked
  • a visible label tied to the input button that we interact with

The CSS acts on the input and label elements and reveals a third part: a :before element on the label.

You’ll see that in the CSS, there are styles for the input, the label, and the label’s :before element.




.star-rating  input {
	display: none;
}
.star-rating > label {
  width: 30px;
  height: 30px;
  display: block;
  position: relative;
  font-family: Verdana;
}
.star-rating label::before {
  content: '\2606';
  font-size: 30px;
  position: absolute;
  top: 0px;
  color: orange;
  line-height: 30px;
}

If you dig deeper into the CSS you’ll find a few additional things:

  • The styles only affect inputs within an element having the star-rating class.

  • The :before pseudoselector places the star on the label’s :before element, rather than the label itself.

  • The line content: '\2606' sets the shape of the hollow star.

  • The font-family of the label element is set to Verdana, and the :before element inherits this. Controlling font is super important for making both hollow and solid stars appear the same size in Chrome and Safari browsers.

  • The position of the label element is set to relative so that the :before element’s absolute position is set based on the label’s position.

One star in the “on” state

Now let’s make a filled star in the “on” state.

Notice that the only difference in the HTML versus the one before it (besides the different ID) is the checked attribute, which is set to true.




.star-rating input {
  display: none;
}
.star-rating > label {
  width: 30px;
  height: 30px;
  display: block;
  position: relative;
  font-family: Verdana;
}
.star-rating label::before {
  content: '\2606';
  font-size: 30px;
  position: absolute;
  top: 0px;
  color: orange;
  line-height: 30px;
}
.star-rating input:checked ~ label:before {
  content:'\2605';
}

There is also a new CSS selector:

.star-rating input:checked ~ label:before

From looking at it, there are a few things to notice:

  • The :checked pseudoselector will select only inputs with checked set to true.
  • The tilda (~) makes the code act not on the input itself, but its siblings; in this case, all the label elements that come after it.
  • The line content: '\2605' sets the shape of the solid star. Let’s take one more look:

.star-rating input:checked ~ label:before {
  content: '\2605';
}

In other words, the content value of \2605 causes the :before element of the affected labels will show up as a filled star rather than a hollow star ().

A Group of Stars

Now let’s try five stars using the same CSS.

Oops! Assuming we want our stars in a row, that’s not what we want.

Stars in a Row

The group below is a little better, because it uses CSS tied to Flexbox (or the flexible box layout model) to make the stars line up in a row.

But try clicking or tapping each star to see what happens - you will find that something is still not quite right about how the stars react.

You’ll see that if you click at a star, the stars to the right, rather than those on the left, end up highlighted. So assuming you want to read the star rating from left to right, this should be fixed.

Before we ponder what’s missing in the code, let’s take a look at what’s helping: the new flexbox CSS at the top.

.star-rating {
  display: flex;
  align-items: center;
}
.star-rating input {
	display: none;
}
.star-rating > label {
  width: 30px;
  height: 30px;
  font-size: 30px;
  display: block;
  position: relative;
  font-family: Verdana;
}
.star-rating label::before {
  content: '\2606';
  position: absolute;
  top: 0px;
  color: orange;
  line-height: 30px;
}
.star-rating input:checked ~ label:before {
  content:'\2605';
}



The additional CSS up top sets display of the star-rating container to flex to line the stars up horizontally rather than vertically. (flex-direction, the CSS property that determines whether the flex layout is a row or a column, is set to row by default.)

It also centers the items vertically with align-items.

And you’ll see that the tilda ~ in .star-rating input:checked ~ label:before is working its magic, but not in quite the right way. It is doing what it naturally does and making all the stars after the selected star also show up as full, but what we really want is to have all the stars before the selected star show up as full.

How do we solve this problem?

Flipping the Group

Let’s add a flex-direction attribute and set it to row-reverse. This reverses the order of stars so that the ones that show up after a given star actually show up before it. Because this turns the “end” of the container into the beginning, let’s also use justify-content to “flex-end” to keep the stars horizontally left-aligned.


.star-rating {
  display: flex;
  align-items: center;
  flex-direction: row-reverse;
  justify-content: flex-end;
}

Stars working right

The stars now highlight in the correct order. If you click a star and make it full, all those to the left of it will also be full.

Notice in the underlying HTML below that the value attributes of my input elements have been in descending order so that when Flexbox reverses their layout, the star values are in the correct ascending order.

For instance, the star with a value of “1” shows up on the left and the star with the value of “5” shows up on the right.

And the CSS has flex-direction: row-reverse and justify-content: flex-end.




.star-rating {
  display: flex;
  align-items: center;
  flex-direction: row-reverse;
  justify-content: flex-end;
}
.star-rating input {
	display: none;
}
.star-rating > label {
  width: 30px;
  height: 30px;
  font-size: 30px;
  display: block;
  position: relative;
  font-family: Verdana;
}
.star-rating label::before {
  content: '\2606';
  position: absolute;
  top: 0px;
  color: orange;
  line-height: 30px;
}
.star-rating input:checked ~ label:before {
  content:'\2605';
}

Going further

Here are a few suggestions for building off of this:

  • Try adding a :hover states with a different color.
  • Try adding an :active state for when a star is pressed.
  • Try adding CSS transitions to smooth out these states.

What might this look like, you ask? Check out this CodePen to find out! Thanks for reading!

Additional Resources

  • There is a great concise tutorial on CSS Tricks for getting started with star rating CSS.

  • If you are new to Flexbox, check out Samantha Ming’s Flexbox in 30 days get a great introduction and dig deeper!