Creating a Star Rating Widget
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
inputbutton that is either checked or not checked - a visible
labeltied 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-ratingclass. -
The
:beforepseudoselector places the star on the label’s:beforeelement, rather than the label itself. -
The line
content: '\2606'sets the shape of the hollow star. -
The
font-familyof thelabelelement is set toVerdana, and the:beforeelement inherits this. Controlling font is super important for making both hollow and solid stars appear the same size in Chrome and Safari browsers. -
The
positionof thelabelelement is set torelativeso that the:beforeelement’sabsoluteposition 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
:checkedpseudoselector will select only inputs withcheckedset totrue. - The tilda (
~) makes the code act not on theinputitself, but its siblings; in this case, all thelabelelements 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
:hoverstates with a different color. - Try adding an
:activestate 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!