In the past, creating custom components required complex combinations of HTML, CSS, and JavaScript. However, the advancement of CSS in recent years, enables us to build many components using just HTML and CSS -leveraging the logic already built into the browsers. Why reinvent the wheel when we can reuse most of it?
Simple components like checkboxes, radio buttons, and toggle switches can be created with HTML and CSS while relying on the browser for functionality. But we are not limited to simple components. More complex components can also be achieved this way.
In this article, we'll explore how to build a star rating system using a single HTML and just one JavaScript command.
The HTML
A star rating component is essentially a range of values from which users can select one. Variations may include 5 values (one per star) or 10 values (allowing half-stars), but the idea remains the same - users can select one value, and only one.
HTML provides an input type designed for ranges, which we can use as the base for our component:
<input type="range">
As it stands, this input isn't very useful. We need to define some attributes based on our design specifications:
- Allow half-star ratings.
- Ranging from 0.5 to 5 stars.
- Default selection will be 2.5 stars.
Based on the specifications above, the HTML will be like this:
<input type="range" min="0.5" max="5" step="0.5" value="2.5">
This component is a range input (<input type="range">
) that allows users to select a value between 0.5 (min="0.5"
) and 5 stars (max="5"
) in increments of 0.5 (step="0.5"
). The initial value is set to 2.5 stars (value="2.5"
).
Setting the minimum value of 0.5 instead of 0 may seem unusual, but there's a practical reason for it. Allowing a 0-star review would create 11 potential values, but the range visually represents 10 values (10 half stars), creating a mismatch between the clickable areas and the stars in the range. This design choice ensures better usability and simplifies the implementation later.
This issue would be partially solved by creating the component using 11 radio buttons. But that would raise new issues in design (how do we select the 0 value using the mouse?), usability (should we mimic the native behavior of a range input or the native behavior of the radio buttons?), and accessibility (how do we manage the focus for the component as a whole?)
These are great questions for another tutorial on how to create a rating component using radio buttons. For this tutorial on how to create the component using a single HTML element, and I opted for a minimum value of 0.5 to avoid these complexities.
We'll add some tweaks later, but this code works as a solid starting point. Without any CSS, it visually resembles a standard range input:
Next, we'll add a few more attributes. While they may not seem significant at the moment, they will be important later:
- Class name: helps identify the range input in CSS.
- Inline styles: with a custom property to store the input's value.
- Inline JavaScript: a single command to update the custom property in the inline style above.
The final code will look like this (formatted for readability):
<input
type="range"
min="0.5"
max="5"
step="0.5"
value="2.5"
class="star-rating"
style="--val: 2.5"
oninput="this.style='--val:'+this.value"
>
The CSS
Styling range inputs can be tricky–but not excessively complex. Unfortunately, it requires a lot of repetition due to lack of support and standardization, so we must use vendor prefixes and browser-specific pseudo-classes for the different elements of the component:
-
thumb: the element user can move to change the value. The pseudo-elements are
::-webkit-slider-thumb
(Chrome and Safari) and::-moz-range-thumb
(Firefox) -
track: the area or line along which the thumb slides. The pseudo-elements are
::-webkit-slider-runnable-track
(Chrome and Safari) and::-moz-range-track
(Firefox)
And, of course, we'll need to apply some specific styles for each browser, as they don't style the component consistently. For example, we'll need to set up heights on Safari or remove a pesky border on Firefox.
From here, the next steps are as follows:
- Defined the size of the star-rating component.
- Mask the track to only keep the shapes of the stars visible.
- Define the background that only colors the selected stars.
- Hide the thumb.
Hiding the thumb is optional and it will depend on the type of component you are building. It makes sense to hide the thumb in this star-rating system. However, in a user-satisfaction component, the thumb may be useful. You can explore different demos at the end of this article.
Styling the range element
The first step will be removing the default appearance of the range input. This can be done that by setting the the appearance:none
property. All modern browsers support it, but we may want to add the vendor-prefixed versions, so it's compatible with older browsers too.
Since we have five stars, it makes sense to set the width to five times the height. aspect-ratio: 5/1
could handle this, but some browsers still have inconsistent support, so we'll "hard code" the size using a custom property.
Additionally, we want to remove the border. Firefox applies a default border to the ranges, and removing it ensures a more consistent styling across browsers.
.star-rating {
--size: 2rem;
height: var(--size);
width: calc(5 * var(--size));
appearance: none;
border: 0;
}
In the next section, we will refine this rule a little bit. We will have to define styles for Chrome/Safari and for Firefox, and that will bring some repetition. We can streamline the process by using custom properties to store values and apply them across both styles.
Styling the track
The track will occupy the entire size of the element, and then we will use a gradient to color the parts we need.
We don't need to worry about the width –it will occupy the whole width of the element–, but he height is a different story. While Chrome and Firefox make the track's height to match the container, Safari does not. So, we will have to explicitly indicate a height of 100%.
Next we want to define the colored area. We will leverage the --val and --size custom properties that we created earlier. We'll set a linear-gradient from left to right that changes colors at the point indicated by the --val property:
linear-gradient(90deg, #000 calc(var(--size) * var(--val)), #ddd 0);
We'll move this gradient to another custom property in the parent element. This allows us to reuse the value for Chrome/Safari and Firefox –as I mentioned before… and will likely mention later.
With this, we have a rectangle with a darker area representing the selected value. The black area changes as we click or slide over the rectangle, which is the desired functionality, yet it lacks the visuals. We need CSS masks.
I won't deny it, the following part is ugly. I chose to go all-in with CSS, without relying on external images or inline SVGs. You could simplify the code by using any of those options.
The following code is for star-shaped rating component, but we could easily change the shape (e.g., to circles), by changing the mask.
We'll use a set of conic gradients to clip a five-point star using CSS masks. It takes into account the size of the range input so, after the mask is repeated horizontally, we will get five stars:
conic-gradient(from -18deg at 61% 34.5%, #0000 108deg, #000 0) 0 / var(--size),
conic-gradient(from 270deg at 39% 34.5%, #0000 108deg, #000 0) 0 / var(--size),
conic-gradient(from 54deg at 68% 56%, #0000 108deg, #000 0) 0 / var(--size),
conic-gradient(from 198deg at 32% 56%, #0000 108deg, #000 0) 0 / var(--size),
conic-gradient(from 126deg at 50% 69%, #0000 108deg, #000 0) 0 / var(--size);
As with the linear gradient above, we'll apply this mask in the styles for both Chrome/Safari and Firefox. To avoid code repetition, we'll define it in a custom property within the parent element.
The final code will look like this:
.star-rating {
--size: 2rem;
--mask: conic-gradient(from -18deg at 61% 34.5%, #0000 108deg, #000 0) 0 0 / var(--size) var(--size),
conic-gradient(from 270deg at 39% 34.5%, #0000 108deg, #000 0) 0 0 / var(--size) var(--size),
conic-gradient(from 54deg at 68% 56%, #0000 108deg, #000 0) 0 0 / var(--size) var(--size),
conic-gradient(from 198deg at 32% 56%, #0000 108deg, #000 0) 0 0 / var(--size) var(--size),
conic-gradient(from 126deg at 50% 69%, #0000 108deg, #000 0) 0 0 / var(--size) var(--size);
--background: linear-gradient(90deg, #000 calc(var(--size) * var(--val)), #ddd 0);
height: var(--size);
width: calc(5 * var(--size));
appearance: none;
border: 0;
}
/* Chrome and Safari */
.star-rating::-webkit-slider-runnable-track {
height: 100%;
mask: var(--mask);
mask-composite: intersect;
background: var(--background);
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* Firefox */
.star-rating::-moz-range-track {
height: 100%;
mask: var(--mask);
mask-composite: intersect;
background: var(--bg);
print-color-adjust: exact;
}
Notice how the code for Webkit and Firefox is nearly identical. A feature like mixins in CSS would be incredibly helpful in situations like this –although having a single supported standard would be even better.
We also added some styles (print-color-adjust: exact
) to ensure that the component is printed exactly as it appears on the screen. This is useful when working with backgrounds, as they are not typically printed by default.
Styling the thumb
In the case of this star-rating system, the thumb is not particularly important. The visual effect is achieved using the track itself. So, we'll hide the thumb from view.
We can do it by setting its opacity to zero:
/* Chrome and Safari */
.star-rating::-webkit-slider-thumb {
opacity: 0;
}
/* Firefox */
.star-rating::-moz-range-thumb {
opacity: 0;
}
You may have noticed that I didn't use CSS nesting in the code snippets above (while I used it in the demos below). This is because nesting is relative new and comes with some limitations: many older browsers don't support it, and also some modern browsers struggle with non-standard pseudo-elements –I reported a bug on WebKit about this behavior on Safari.
Examples
They say a picture is worth a thousand words. So, here are some examples of input ranges that can be coded using a single HTML element.
Let's start with the star-rating component described in this article:
Next, a colorful example. It has an atypical shape and it requires styling all the parts of a range input: the range in itself, the track, and the thumb:
Finally, a favorite of mine: an animated, single element user-satisfaction component. Pick any of the different faces and they will move based on selection:
Conclusion
There are many different ways to code this component. I have done it using HTML and CSS only (with one inline JavaScript command), but you could also use images or more JavaScript to prevent that ugly inlining.
The key idea is that, with minimal HTML and CSS changes, you can adjust the specifications, and your star-rating system will behave slightly differently or look completely different.
The component can also be recreated using multiple radio buttons, which would eliminate the need for the JavaScript line and some CSS. It is a valid approach. It would require additional code to ensure accessibility and replicate the default behavior that comes out-of-the-box with an input range. Some may find this approach easier, and it is certainly doable.
That's one of the things I love most about software development: there are many different approaches and options, each with its pros and cons, and all are beautifully achievable.
I hope you enjoyed the article. Keep coding!
Top comments (25)
Nice! Almost 4 years ago a bunch of us did a silly set of posts making star rating components. We got a little carried away with it but it was fun.
@grahamthedev @madsstoumann @afif can you believe it was that long ago?!
Damn, time flies!
Maybe time to do something like that again? 🤓
Oh you don't want a piece of this :-) hahah.
Time just flies, shows how little I have written lately as that felt quite recent!
That time when my stellar ideas were killing everyone! 😈
hmmm, I think you and I remember things differently 😜
No way! Alzheimer at your age?!! .. poor you 😧🤪
I’m in!
So what are we making?
I did the Solar System, Fibonaccis Spiral and the Periodic Table in CSS last year, and have been struggling coming up with something as recognizable (and fun!) as these — any ideas?
Inspired by my savings in the Lidl-app, I did this many months ago: browser.style/ui/progress-meter/
I'm sure Temani can do it with less or no JavaScript ;-)
That's a widget to display things... why does it use inputs behind the scenes when you can't click it? I am confuse.
Consider it an unfinished sketch, that’s why I mention it here — not sure if it should be interactive? Or a range-slider with custom ticks?
A, progress meter could be good, but leave it as wide as that rather than a particular type (ultimate progress meter).
Saying that, the only thing anybody builds nowadays is AI chatbots, so how about we just do a competition around "chat" (yet again, as wide as possible so we get the silly stuff again!)? 😁
I missed that party.
My <star-rating> was 22 lines of JavaScript in 2021
dev.to/dannyengelman/twinkle-twink...
This predates us by a month 😮
I consider you an honorary part of the competition now I have seen that...and you lost just the same as everyone else! 😱🤣💗
Story of my internet life:
I do things long before anyone else, and all I get out of it is the ability to say I did something long before everyone else.
I once had the domain name photomatchmaker.com. It was supposed to be a site where you could rate, like, or rank photographs.
Eight years later, I emailed Mark... but never got a reply.
Well now you are part of the competition you get the same level of response I would give everyone else:
Just because you were first does not mean your submission sucked any less than any of the others, 😱🤣💗
🎉 True Happiness for Developers!
There’s no better feeling than seeing your code run flawlessly on the first attempt! 🙌 At CultSoft, we celebrate these little wins that make the coding journey worthwhile.
💻 Let’s continue to code, debug, and innovate together, one successful execution at a time!
What’s your happiest coding moment? Share it in the comments below! 😊
CultSoft #CodingHappiness #DeveloperLife #NoErrors #TechCommunity
None of the star rating components allow for 0 stars, this reduces the effectiveness of such a rating as you will always have a bias to a favorable rating. This control might have a smaller one than most, but it still has a bias. I want to see a rating system with 5 solid stars and 1 hollow one. It would be a little more work, since the first hollow star would need to not allow ½ highlighting, and most likely you wouldn't want it to be highlighted when there is a value other than 0. So, the best solution for this problem is to have that case handled differently, most likely with JavaScript.
As final step may upgrade to a WebComponent.
All post is great. Final step of hours of operation amazing.
Very nice.
noiice!! VERY cool semantic CSS-first implementation! I Approve. 💯
🤞
Some comments may only be visible to logged-in visitors. Sign in to view all comments.