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
(andvalueAsNumber
in 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 usingbox-shadow
can be used. The example above has been updated to include both types. You can also set it toreadonly
, 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)
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... !
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.
LOL. It is definitely possible.
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: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.
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'sconsole
:Best wishes,
Mads
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
js - version output
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?Best wishes,
Mads
@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.
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!
@madsstoumann Please resurrect 🙈 your autosuggest plugin, yours is superior to any other and will be looking forward to the new and improved version 🙏😊🫣
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?
Not sure how … it use CSS mask, so the “whole star” is masked, you cannot separate the border.
Dears, is it possible to add and url to each of the stars, so when you click on it - it opens an url?
No, sorry, that would require a different solution.
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])
Or the “change” or “input” events, ie
input.addEventListener('change', event => { do something with input.value })
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
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.
OK, found the iPhone-issue. I had written
pacity
instead ofopacity
for the thumb. Beers on me.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!
I don’t do anything with IE-support anymore, but I guess a few clients still need it?
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!
Please no more IE. Let it die 🙏🏾. Let it be history please.
Amen!
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!
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 😂.
… and I guess Temani Afif is cooking up something as well! 😂
Oh damn 😂
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.
Thank you! The principle is the same as my Range-collection and image compare – updating a single CSS Custom Property.
Sneaking in a little bit of JavaScript I see 😅
I don't write “without JavaScript” anywhere! But 45 bytes won't kill performance 😁
Yeah I know, I don't mind a bit of JS here and there 😉
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:
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 aswidth:100%
,border
etc.