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
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 thelabel
element is set toVerdana
, 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 thelabel
element is set torelative
so that the:before
element’sabsolute
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 withchecked
set totrue
. - The tilda (
~
) makes the code act not on theinput
itself, but its siblings; in this case, all thelabel
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!