DEV Community

Cover image for Single HTML Element Star Rating Component
Alvaro Montoro
Alvaro Montoro

Posted on

Single HTML Element Star Rating Component

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

As it stands, this input isn't very useful. We need to define some attributes based on our design specifications:

  1. Allow half-star ratings.
  2. Ranging from 0.5 to 5 stars.
  3. 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">
Enter fullscreen mode Exit fullscreen mode

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.

comparison of star-rating systems with 10 and 11 clicking areas

Setting a minimum of 0.5 allows for more natural clicking areas

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:

Screenshot of an input range half selected.

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

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:

  1. Defined the size of the star-rating component.
  2. Mask the track to only keep the shapes of the stars visible.
  3. Define the background that only colors the selected stars.
  4. 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;
}
Enter fullscreen mode Exit fullscreen mode

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

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.

black and gray rectangle

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

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

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

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)

Collapse
 
link2twenty profile image
Andrew Bone • Edited

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

Collapse
 
madsstoumann profile image
Mads Stoumann • Edited

Damn, time flies!

Collapse
 
alvaromontoro profile image
Alvaro Montoro

Maybe time to do something like that again? 🤓

Thread Thread
 
grahamthedev profile image
GrahamTheDev

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!

Thread Thread
 
afif profile image
Temani Afif

That time when my stellar ideas were killing everyone! 😈

Thread Thread
 
grahamthedev profile image
GrahamTheDev

hmmm, I think you and I remember things differently 😜

Thread Thread
 
afif profile image
Temani Afif

No way! Alzheimer at your age?!! .. poor you 😧🤪

 
madsstoumann profile image
Mads Stoumann

I’m in!

Thread Thread
 
link2twenty profile image
Andrew Bone

So what are we making?

Thread Thread
 
madsstoumann profile image
Mads Stoumann

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

Thread Thread
 
moopet profile image
Ben Sinclair

That's a widget to display things... why does it use inputs behind the scenes when you can't click it? I am confuse.

Thread Thread
 
madsstoumann profile image
Mads Stoumann

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?

Thread Thread
 
grahamthedev profile image
GrahamTheDev

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!)? 😁

Collapse
 
dannyengelman profile image
Danny Engelman

I missed that party.

My <star-rating> was 22 lines of JavaScript in 2021

dev.to/dannyengelman/twinkle-twink...

Collapse
 
link2twenty profile image
Andrew Bone

This predates us by a month 😮

Collapse
 
grahamthedev profile image
GrahamTheDev

I consider you an honorary part of the competition now I have seen that...and you lost just the same as everyone else! 😱🤣💗

Thread Thread
 
dannyengelman profile image
Danny Engelman

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.

Thread Thread
 
grahamthedev profile image
GrahamTheDev

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, 😱🤣💗

Collapse
 
thecultsoft profile image
CiltSoft

Image description
🎉 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

Collapse
 
jeffrey_tackett_5ef1a0bdf profile image
Jeffrey Tackett

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.

Collapse
 
pengeszikra profile image
Peter Vivo

As final step may upgrade to a WebComponent.

Collapse
 
wasif_ali_d2a4d12bbaca251 profile image
Wasif Ali • Edited

All post is great. Final step of hours of operation amazing.

Collapse
 
moopet profile image
Ben Sinclair

Very nice.

Collapse
 
epic-win profile image
EPIC WIN Productions Media

noiice!! VERY cool semantic CSS-first implementation! I Approve. 💯

Collapse
 
ashish567 profile image
Ashish567

🤞

Some comments may only be visible to logged-in visitors. Sign in to view all comments.