Star-Rating Using A Single Input

Yesterday I read InhuOfficial's post about star-rating, using a group of <input type="radio">-controls. Go read that for some great accessibility-insights.

I did something similar a couple of years ago, also using radio-buttons, but with the unicode:bidi / direction-hack to select the previous elements on :hover.
On Codepen, you'll find more examples.

But it made me think: Is there another, perhaps simpler way, to create a rating-control?

Earlier this year, I did this image compare, where a single <input type="range"> controls two clip-path's.

That would also work as a rating-control, where the “left” image is the “filled stars” and the “right” image is the “unfilled stars”.

What are the advantages of using an <input type="range">?

  • It's keyboard-accessible, can be controlled with all four arrow-keys
  • It's touch-friendly
  • It returns a value (and valueAsNumberin JavaScript), great for both visual browsers and screen-readers.

Let's dive into how we can use an <input type="range"> for a rating-control. We'll make one, where you can easily add more stars, use half or even quarter-star rating, customize the star-colors etcetera.

The HTML

<label class="rating-label">
  <strong>Rating</strong>
  <input
    class="rating"
    max="5"
    oninput="this.style.setProperty('--value', this.value)"
    step="0.5"
    type="range"
    value="1">
</label>

The max is used for ”how many stars”. The step is 1 by default, but in this case, it's been set to 0.5, allowing “half stars”. The oninput can be moved to an eventListener, if you want. It returns the current value and sets it as a “CSS Custom Property”: --value.

The CSS

The first thing we need, is a star:

--star: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 17.25l-6.188 3.75 1.641-7.031-5.438-4.734 7.172-0.609 2.813-6.609 2.813 6.609 7.172 0.609-5.438 4.734 1.641 7.031z"/></svg>');

This is an SVG, used in a CSS url(), so we can use it as a mask in mutiple places.

The fill of the stars and the default background-fill (when a star is not selected) are set as properties too:

--fill: gold;
--fillbg: rgba(100, 100, 100, 0.15);

And finally, we need some default sizes and values:

--dir: right;
--stars: 5;
--starsize: 3rem;
--symbol: var(--star);
--value: 1;
--x: calc(100% * (var(--value) / var(--stars)));

The --x variable is essential, as this indicates the “cutting point” in the gradient, we'll use in the “track” of the range-slider:

.rating::-webkit-slider-runnable-track {
  background: linear-gradient(to var(--dir), var(--fill) 0 var(--x), var(--fillbg) 0 var(--x));
  block-size: 100%;
  mask: repeat left center/var(--starsize) var(--symbol);
  -webkit-mask: repeat left center/var(--starsize) var(--symbol);
}

And that's basically it! The linear-gradient is “filling up” the stars with the --fill-color, while the mask is used to mask it as stars.

But why the --dir-property in the linear-gradient?

That's because we can't set a logical direction in CSS-gradients, for instance:

linear-gradient(to inline-end, ...)

… does not work (yet!). Therefore, in order to make it work with “right-to-left”-languages, we need the --dir-property:

[dir="rtl"] .rating {
  --dir: left;
}

In this case, when the dir is rtl, the gradient will be “to left”.

Here's a Codepen demo – notice how easy it is to add more stars, and how you can “drag” it as a slider:

UPDATE: People have requested a non-JS version, although the JS is only 45 bytes. Chrome does not support range-progress (like Firefox), but a hack using box-shadow can be used. The example above has been updated to include both types. You can also set it to readonly, if you want to show an “average review rating” like the last of the examples above.

And – to honor InhuOfficial:

Thanks for reading!

Cover-photo by Sami Anas from Pexels

16