DEV Community

Cover image for Star-Rating Using A Single Input
Mads Stoumann
Mads Stoumann

Posted on • Updated on

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>
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

Top comments (42)

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

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
 
yaddly profile image
Admire Mhlaba

Hi @madsstoumann our frontend maestro, thank you for this wonderful post. Kindly advise what could I be doing wrong because I tried using JavaScript to reset the star-rating value to 0 using the following code:

$(this).val(0);
$(this).css("--value", 0);
Enter fullscreen mode Exit fullscreen mode

I get this behavior on both options rating and rating-nojs classes. The star-rating input does remove the stars but the input value does not get reset to 0, it stays the same. Kindly advise what could be going amiss.

Image description

Collapse
 
madsstoumann profile image
Mads Stoumann

Hi,
In Vanilla JS, it's enough to set the custom property, since it's from that property the mask is "drawn". As an example, select the <input> in Dev Tools and paste this in it's console:

$0.style.setProperty('--value', 0)
Enter fullscreen mode Exit fullscreen mode

Best wishes,
Mads

Collapse
 
yaddly profile image
Admire Mhlaba

Hi @madsstoumann Thank you for your swift and kind response. I have tried out the suggestion and it clears the stars but not the input value. Please see images below for their respective output. Perhaps some contextual background would help. I'm displaying this star-rating input on a bootstrap modal. I'm posting invalid data to the server running a Laravel 10 application.

The app validates the data and rejects it, and sends it back to the client. I'm using JavaScript to show the bootstrap modal again and display validation errors on each input along with previous input values.

When I click cancel button to dismiss the modal after this sequence of events, that is when the star-rating input fails to reset the value. But is not a train smash, the old value is overridden when I select a new rating.

nojs - version output

Image description

js - version output

Image description

Thread Thread
 
madsstoumann profile image
Mads Stoumann

Hi again,
You need to reset the value as well.
If you set an initial value, that value will be set on the form's reset-event, so maybe hook into that?

form.addEventListener('reset', (event) => {
    form.elements.ratingScore.style.setProperty('--value', 0);
})
Enter fullscreen mode Exit fullscreen mode

Best wishes,
Mads

Collapse
 
yaddly profile image
Admire Mhlaba • Edited

@madsstoumann This is the best star rating article, example and working demo on the entire internet 🌍. I'm not a frontend developer, but I was able to add this to my demo blog app.

Your ability to simplify difficult to implement concepts is your ultimate elegance and sophistication. You're our tech maestro

By any chance, may we please have a tutorial on autocomplete and multiselect dropdown with data searching capabilities. A good example would be this plugin but it is no longer supported and working with bootstrap 5.

Collapse
 
madsstoumann profile image
Mads Stoumann

Thank you so much for the kind words. I did write about autosuggest a while ago, but it might need an update. I also have a playground, where you can find an Autosuggest!

Collapse
 
yaddly profile image
Admire Mhlaba

@madsstoumann Please resurrect 🙈 your autosuggest plugin, yours is superior to any other and will be looking forward to the new and improved version 🙏😊🫣

Collapse
 
juryha profile image
JuryHa

Wicked solution! I would like to add border(better to use word stroke) for each star but didn't really come to working solution. Is there a way to add it?

Collapse
 
madsstoumann profile image
Mads Stoumann

Not sure how … it use CSS mask, so the “whole star” is masked, you cannot separate the border.

Collapse
 
glebskozlovskis profile image
Glebs Kozlovskis

Dears, is it possible to add and url to each of the stars, so when you click on it - it opens an url?

Collapse
 
madsstoumann profile image
Mads Stoumann

No, sorry, that would require a different solution.

Collapse
 
jamievaughn profile image
Jamie Vaughn

You could add a click listener that checks the value on click and accessing some url by index based on the value: for example: (e) => window.open(urls[e.target.value])

Collapse
 
madsstoumann profile image
Mads Stoumann • Edited

Or the “change” or “input” events, ie input.addEventListener('change', event => { do something with input.value })

Collapse
 
grahamthedev profile image
GrahamTheDev

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 • 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

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

Thread Thread
 
grahamthedev profile image
GrahamTheDev

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

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

Thread Thread
 
grahamthedev profile image
GrahamTheDev

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

Amen!

Thread Thread
 
grahamthedev profile image
GrahamTheDev

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
 
lakbychance 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

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

Collapse
 
lakbychance profile image
Lakshya Thakur

Oh damn 😂

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

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

Collapse
 
link2twenty profile image
Andrew Bone

Sneaking in a little bit of JavaScript I see 😅

Collapse
 
madsstoumann profile image
Mads Stoumann • 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
 
fmarz96 profile image
fmarz96

I wanted to ask you something. I am trying to implement this input range and worked perfectly but when I add bulma classes or width: 100% to .rating(the input class) it repeats the mask all over the input. What should I do? What I want is that the mask shows only the 5 stars but with the input type="range" has width:100% .
Here's the codepen link:

Collapse
 
madsstoumann profile image
Mads Stoumann

That's sort of how "the hack" works, juts repeating the mask with the width of a star times the amount of stars. You need to update the --starwidth variable, but it'll look awkward. I'd wrap the input in a <label>, and then style that as width:100%, border etc.