Easy Star Ratings in CSS
The Basic Star
.rating_selection %input(type='radio' name='rating' value="rating_1" id="rating_1") %label(for="rating_1")> %span Rate 1 Star %input(type='radio' name='rating' value="rating_2" id="rating_2") %label(for="rating_2")> %span Rate 2 Stars / ... and so on
You'll note that each label has a span inside. This is for two reasons:
- We're going to be using a
:beforeelement on the
labelto display our stars and make them clickable (
:beforeelements on inputs behave strangely)
- We need to hide the text of the label (the label text isn't absolutely necessary, but is useful for accessibility & testing).
The star itself is going to be a character in the
content attribute of the label's
:before element. Normally I'd use an icon font here, but since this is just an example, we'll use the standard ★ character.
One other detail: note the
> character after each
label. This is a special HAML character that removes white space in the HTML output, so our
inline-block elements will render directly next to one another.
Here's the Sass for our basic star rating:
.rating_selection input[type='radio'], span display: none label cursor: pointer &:before display: inline-block content: "★" font-size: 80px letter-spacing: 10px color: #e9cd10
This will give you a row of yellow stars. Hooray!
Selecting a Star Rating
The next challenge is how to display selected stars. You'll note that our current stars are all yellow: this is because CSS can only look forward, not behind, so we have to use CSS rules to grey out stars AFTER our currently selected star. We have two overall states to account for:
- when I select a star, I should see that star and all previous stars highlighted.
- when I hover over a star, regardless of what is currently selected, I should see that star and all previous stars highlighted.
The first state is relatively simple, thanks to some sneaky uses of CSS sibling selectors.
.rating_selection input:checked + label ~ label:before color: #aaa
This amalgamation of selectors works because of our flat DOM structure. It looks forward from our currently checked input PAST the direct sibling (that's the
+ label) part, and then selects ALL following
label:before elements (that's the magic of the
~ selector: it selects all following siblings, regardless of what other content might also be present).
So now our stars will highlight properly when clicked.
Adding a Hover State
The hover state is a little trickier: we want the user to be able to hover over any star and see the appropriate amount of stars highlighted. To do that, we needs to disregard the currently selected star completely. We'll need to be more specific in our selector chain, in order to override the selection rules. (You could also use the
!important crutch here, but we'll rise above that.)
We'll add our rules to a
:hover state on the containing
.rating_selection element, and use an attribute selector on the
label to make it more specific than our previous rule.
label[for] here is a very safe option, as labels should always have a
.rating_selection &:hover // make all labels yellow again label[for]:before color: #e9cd10 // grey out all labels after hovered label label:hover ~ label:before color: #aaa
Accounting for unrated items
The last challenge here is to allow for a state where a rating hasn't taken place yet. This is actually pretty straightforward: we'll just add a
rating_0 radio, and hide it.
.rating_selection %input(type='radio' name='rating' value="rating_0" id="rating_0" checked) %label(for="rating_0")> %span Unrated // other rating elements here
To hide it, we'll just hide the first label using the
:first-of-type selector. That's all we have to add, since radio inputs are already hidden.
.rating_selection label:first-of-type display: none