DEV Community

loading...
Cover image for Star-Rating Using A Single Input

Star-Rating Using A Single Input

Mads Stoumann
I'm a web developer, graphic designer, type designer, musician, comicbook-geek, LEGO-collector, food lover … as well as husband and father, located just south of Copenhagen, Denmark.
Updated on ・3 min read

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>
Enter fullscreen mode Exit fullscreen mode

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>');
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)));
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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, ...)
Enter fullscreen mode Exit fullscreen mode

… 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;
}
Enter fullscreen mode Exit fullscreen mode

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

Discussion (26)

Collapse
afif profile image
Temani Afif • Edited

ok, so there is a game of "rating stars" here. I have to participate with a non-JS and non-SVG solution 😉

UPDATE: here I am: dev.to/afif/scalable-star-rating-w... !

Collapse
madsstoumann profile image
Mads Stoumann Author

LOL! No competition at all, just a different way of doing it!
The "image-compare"-version will work with any type of image. What I like with the gradient/mask-type is the flexibility.
If I want 20 stars, it's just updating an attribute.

Collapse
hasnaindev profile image
Muhammad Hasnain

LOL. It is definitely possible.

Collapse
inhuofficial profile image
InHuOfficial

Love the concept of this, but weird on iPhone as where you tap becomes white but I am sure that could be fiddled with.

I am not sure why I didn’t think of using a range slider. Guess what I am going to be fiddling with to see if I can take your concept and make it work perfectly!

P.S. the “thanks for reading” was a nice touch, made me chuckle

Collapse
madsstoumann profile image
Mads Stoumann Author • Edited

Yes, I updated the post with a small disclaimer!
I hatched up the idea this morning, and didn't test on iPhone - but will look into it soon.

Collapse
madsstoumann profile image
Mads Stoumann Author

OK, found the iPhone-issue. I had written pacity instead of opacity for the thumb. Beers on me.

Thread Thread
inhuofficial profile image
InHuOfficial

Glad you spotted it problem is indeed solved, I never even attempt to debug on my phone 😜🤣

As I said before the principle excites me and although IE support would be horrendous to implement this could well be a pattern that works well!

I need to get to the test bench next week and see if it behaves as well as I think it will!

Thread Thread
madsstoumann profile image
Mads Stoumann Author

I don’t do anything with IE-support anymore, but I guess a few clients still need it?

Thread Thread
inhuofficial profile image
InHuOfficial

It’s more my thing that due to the fact that a lot of JAWS users still use IE.

Small projects I don’t bother apart from IE11, but anything with 1million plus turnover the extra work for IE9 and 10 pays for itself so it is worth it. Bear in mind I do more e-commerce than SAAS so the JS requirements are never horrendous!

Thread Thread
micode360 profile image
Micode • Edited

Please no more IE. Let it die 🙏🏾. Let it be history please.

Thread Thread
madsstoumann profile image
Mads Stoumann Author

Amen!

Thread Thread
inhuofficial profile image
InHuOfficial

I agree with the sentiment but I have no control over what some people use (due to lack of technical knowledge) or are forced to use (due to compatibility with screen reader technology) so while they are still in use I will always try to support them.

With that being said support vs perfection is a very different thing, as long as you can use it and get to the end goal it doesn't matter much if it looks weird etc.

I will be covering this in my (soon to be released) rebuttal piece!

Collapse
lapstjup profile image
Lakshya Thakur

I made that Vanilla JS one which lacked css and accessibility (had keyboard one but not for screen readers). I am glad it has lead to senior folks sharing better accessible approaches. Now I have InhuOffical and your implementation to understand 😂.

Collapse
madsstoumann profile image
Mads Stoumann Author

… and I guess Temani Afif is cooking up something as well! 😂

Collapse
lapstjup profile image
Lakshya Thakur

Oh damn 😂

Collapse
link2twenty profile image
Andrew Bone

Sneaking in a little bit of JavaScript I see 😅

Collapse
madsstoumann profile image
Mads Stoumann Author • Edited

I don't write “without JavaScript” anywhere! But 45 bytes won't kill performance 😁

Collapse
link2twenty profile image
Andrew Bone

Yeah I know, I don't mind a bit of JS here and there 😉

Collapse
koenahn profile image
Koen Ahn

I haven't ever before seen this kind of dynamic js-driven use of CSS custom properties. Using a custom property to set the gradient is genius! Saves you from writing a ton of CSS selectors for different values.

I'm also a big fan of the simplicity of your solution.

Collapse
madsstoumann profile image
Mads Stoumann Author

Thank you! The principle is the same as my Range-collection and image compare – updating a single CSS Custom Property.

Collapse
sebbdk profile image
Sebastian Vargr

Really neat trick, i really like these none js solutions, it usually means the part is compatible with pretty much every library on the web. :)

Collapse
madsstoumann profile image
Mads Stoumann Author • Edited

There's a tiny bit of JS, that update the CSS Custom Property, but most of the logic is in CSS, based on that property!

Collapse
melfordd profile image
Melford Birakor

Wow... Thanks

Collapse
madsstoumann profile image
Mads Stoumann Author

👍🏻

Collapse
charlenebx profile image
Charlène Bonnardeaux

Awesome idea! Thanks a lot!!

Collapse
madsstoumann profile image
Mads Stoumann Author

😁