Design
Easy Star Ratings in CSS
This pattern comes up regularly for us: providing the ability for a user to rate something via stars. Back in the day, we'd use a repeating background image and some JavaScript, but it's 2015 now, and in the world of retina screens and CSS3, we can do better.
The Basic Star
A star rating is just fancy UI allowing the user to select from a set of values (typically 1-5). Instead of using Javascript to write the user's selection back to a hidden input, we're going to skin a list of radio inputs. Since radios need a common name and unique values & ids, we'll use the arbitrary name rating
here.
.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
:before
element on thelabel
to display our stars and make them clickable (:before
elements 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 for
attribute.
.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
And that's it! Scalable, vector, JavaScript-free rating stars in 40 total lines of code. Here's a CodePen with the full example. I hope this will prove useful to you. And Happy New Year!