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
Discussion (27)
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.
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 😂
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 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.
Awesome idea! Thanks a lot!!
😁
Wow... Thanks
👍🏻
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. :)
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!
Dears, is it possible to add and url to each of the stars, so when you click on it - it opens an url?