Last Updated: February 25, 2016
·
744
· cameron

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 the label 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!

(Originally posted on The Hashrocket Blog)