Five Methods for Five-Star Ratings

Avatar of Alfred Genkin
Alfred Genkin on (Updated on )

📣 Freelancers, Developers, and Part-Time Agency Owners: Kickstart Your Own Digital Agency with UACADEMY Launch by UGURUS 📣

In the world of likes and social statistics, reviews are a very important method for leaving feedback. Users often like to know the opinions of others before deciding on items to purchase themselves, or even articles to read movies to see, or restaurants to dine.

Developers often struggle with reviews — it is common to see inaccessible and over-complicated implementations. Hey, CSS-Tricks has a snippet for one that’s now bordering on a decade.

Let’s walk through new, accessible and maintainable approaches for this classic design pattern. Our goal will be to define the requirements and then take a journey on the thought-process and considerations for how to implement them.

Scoping the work

Did you know that using stars as a rating dates all the way back to 1844 when they were first used to rate restaurants in Murray’s Handbooks for Travellers — and later popularized by Michelin Guides in 1931 as a three-star system? There’s a lot of history there, so no wonder it’s something we’re used to seeing!

There are a couple of good reasons why they’ve stood the test of time:

  1. Clear visuals (in the form of five hollow or filled stars in a row)
  2. A straightforward label (that provides an accessible description, like aria-label)

When we implement it on the web, it is important that we focus on meeting both of those outcomes.

It is also important to implement features like this in the most versatile way possible. That means we should reach for HTML and CSS as much as possible and try to avoid JavaScript where we can. And that’s because:

  1. JavaScript solutions will always differ per framework. Patterns that are typical in vanilla JavaScript might be anti-patterns in frameworks (e.g. React prohibits direct document manipulation).
  2. Languages like JavaScript evolve fast, which is great for the community, but not so great articles like this. We want a solution that’s maintainable and relevant for the long haul, so we should base our decisions on consistent, stable tooling.

Methods for creating the visuals

One of the many wonderful things about CSS is that there are often many ways to write the same thing. Well, the same thing goes for how we can tackle drawing stars. There are five options that I see:

  • Using an image file
  • Using a background image
  • Using SVG to draw the shape
  • Using CSS to draw the shape
  • Using Unicode symbols

Which one to choose? It depends. Let’s check them all out.

Method 1: Using an image file

Using images means creating elements — at least 5 of them to be exact. Even if we’re calling the same image file for each star in a five-star rating, that’s five total requests. What are the consequences of that?

  1. More DOM nodes make document structure more complex, which could cause a slower page paint. The elements themselves need to render as well, which means either the server response time (if SSR) or the main thread generation (if we’re working in a SPA) has to increase. That doesn’t even account for the rendering logic that has to be implemented.
  2. It does not handle fractional ratings, say 2.3 stars out of 5. That would require a second group of duplicated elements masked with clip-path on top of them. This increases the document’s complexity by a minimum of seven more DOM nodes, and potentially tens of additional CSS property declarations.
  3. Optimized performance ought to consider how images are loaded and implementing something like lazy-loading) for off-screen images becomes increasingly harder when repeated elements like this are added to the mix.
  4. It makes a request, which means that caching TTLs should be configured in order to achieve an instantaneous second image load. However, even if this is configured correctly, the first load will still suffer because TTFB awaits from the server. Prefetch, pre-connect techniques or the service-worker should be considered in order to optimize the first load of the image.
  5. It creates minimum of five non-meaningful elements for a screen reader. As we discussed earlier, the label is more important than the image itself. There is no reason to leave them in the DOM because they add no meaning to the rating — they are just a common visual.
  6. The images might be a part of manageable media, which means content managers will be able to change the star appearance at any time, even if it’s incorrect.
  7. It allows for a versatile appearance of the star, however the active state might only be similar to the initial state. It’s not possible to change the image src attribute without JavaScript and that’s something we’re trying to avoid.

Wondering how the HTML structure might look? Probably something like this:

<div class="Rating" aria-label="Rating of this item is 3 out of 5">
  <img src="/static/assets/star.png" class="Rating--Star Rating--Star__active">
  <img src="/static/assets/star.png" class="Rating--Star Rating--Star__active">
  <img src="/static/assets/star.png" class="Rating--Star Rating--Star__active">
  <img src="/static/assets/star.png" class="Rating--Star">
  <img src="/static/assets/star.png" class="Rating--Star">
</div>

In order to change the appearance of those stars, we can use multiple CSS properties. For example:

.Rating--Star {
  filter: grayscale(100%); // maybe we want stars to become grey if inactive
  opacity: .3; // maybe we want stars to become opaque
}

An additional benefit of this method is that the <img> element is set to inline-block by default, so it takes a little bit less styling to position them in a single line.

Accessibility: ★★☆☆☆
Management: ★★★★☆
Performance: ★☆☆☆☆
Maintenance: ★★★★☆
Overall: ★★☆☆☆

Method 2: Using a background image

This was once a fairly common implementation. That said, it still has its pros and cons.

For example:

  1. Sure, it’s only a single server request which alleviates a lot of caching needs. At the same time, we now have to wait for three additional events before displaying the stars: That would be (1) the CSS to download, (2) the CSSOM to parse, and (3) the image itself to download.
  2. It’s super easy to change the state of a star from empty to filled since all we’re really doing is changing the position of a background image. However, having to crack open an image editor and re-upload the file anytime a change is needed in the actual appearance of the stars is not the most ideal thing as far as maintenance goes.
  3. We can use CSS properties like background-repeat property and clip-path to reduce the number of DOM nodes. We could, in a sense, use a single element to make this work. On the other hand, it’s not great that we don’t technically have good accessible markup to identify the images to screen readers and have the stars be recognized as inputs. Well, not easily.

In my opinion, background images are probably best-used complex star appearances where neither CSS not SVG suffice to get the exact styling down. Otherwise, using background images still presents a lot of compromises.

Accessibility: ★★★☆☆
Management: ★★★★☆
Performance: ★★☆☆☆
Maintenance: ★★★☆☆
Overall: ★★★☆☆

Method 3: Using SVG to draw the shape

SVG is great! It has a lot of the same custom drawing benefits as raster images but doesn’t require a server call if it’s inlined because, well, it’s simply code!

We could inline five stars into HTML, but we can do better than that, right? Chris has shown us a nice approach that allows us to provide the SVG markup for a single shape as a <symbol></symbol> and call it multiple times with with <use></use>.

<!-- Draw the star as a symbol and remove it from view -->
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
  <symbol id="star" viewBox="214.7 0 182.6 792">
    <!-- <path>s and whatever other shapes in here -->  
  </symbol>
</svg>
    
<!-- Then use anywhere and as many times as we want! -->
<svg class="icon">
  <use xlink:href="#star"></use>
</svg>

<svg class="icon">
  <use xlink:href="#star"></use>
</svg>

<svg class="icon">
  <use xlink:href="#star"></use>
</svg>

<svg class="icon">
  <use xlink:href="#star"></use>
</svg>

<svg class="icon">
  <use xlink:href="#star"></use>
</svg>

What are the benefits? Well, we’re talking zero requests, cleaner HTML, no worries about pixelation, and accessible attributes right out of the box. Plus, we’ve got the flexibility to use the stars anywhere and the scale to use them as many times as we want with no additional penalties on performance. Score!

The ultimate benefit is that this doesn’t require additional overhead, either. For example, we don’t need a build process to make this happen and there’s no reliance on additional image editing software to make further changes down the road (though, let’s be honest, it does help).

Accessibility: ★★★★★
Management: ★★☆☆☆
Performance: ★★★★★
Maintenance: ★★★★☆
Overall: ★★★★☆

Method 4: Using CSS to draw the shape

This method is very similar to background-image method, though improves on it by optimizing drawing the shape with CSS properties rather than making a call for an image. We might think of CSS as styling elements with borders, fonts and other stuff, but it’s capable of producing ome pretty complex artwork as well. Just look at Diana Smith’s now-famous “Francine” portrait.

Francine, a CSS replica of an oil painting done in CSS by Diana Smith (Source)

We’re not going to get that crazy, but you can see where we’re going with this. In fact, there’s already a nice demo of a CSS star shape right here on CSS-Tricks.

See the Pen
Five stars!
by Geoff Graham (@geoffgraham)
on CodePen.

Or, hey, we can get a little more crafty by using the clip-path property to draw a five-point polygon. Even less CSS! But, buyer beware, because your cross-browser support mileage may vary.

See the Pen
5 Clipped Stars!
by Geoff Graham (@geoffgraham)
on CodePen.

Accessibility: ★★★★★
Manangement: ★★☆☆☆
Performance: ★★★★★
Maintenance: ★★☆☆☆
Overall: ★★★☆☆

Method 5: Using Unicode symbols

This method is very nice, but very limited in terms of appearance. Why? Because the appearance of the star is set in stone as a Unicode character. But, hey, there are variations for a filled star (★) and an empty star (☆) which is exactly what we need!

Unicode characters are something you can either copy and paste directly into the HTML:

See the Pen
Unicode Stars!
by Geoff Graham (@geoffgraham)
on CodePen.

We can use font, color, width, height, and other properties to size and style things up a bit, but not a whole lot of flexibility here. But this is perhaps the most basic HTML approach of the bunch that it almost seems too obvious.

Instead, we can move the content into the CSS as a pseudo-element. That unleashes additional styling capabilities, including using custom properties to fill the stars fractionally:

See the Pen
Tiny but accessible 5 star rating
by Fred Genkin (@FredGenkin)
on CodePen.

Let’s break this last example down a bit more because it winds up taking the best benefits from other methods and splices them into a single solution with very little drawback while meeting all of our requirements.

Let’s start with HTML. there’s a single element that makes no calls to the server while maintaining accessibility:

<div class="stars" style="--rating: 2.3;" aria-label="Rating of this product is 2.3 out of 5."></div>

As you may see, the rating value is passed as an inlined custom CSS property (--rating). This means there is no additional rendering logic required, except for displaying the same rating value in the label for better accessibility.

Let’s take a look at that custom property. It’s actually a conversion from a value value to a percentage that’s handled in the CSS using the calc() function:

--percent: calc(var(--rating) / 5 * 100%);

I chose to go this route because CSS properties — like width and linear-gradient — do not accept <number></number> values. They accept <length></length> and <percentage></percentage> instead and have specific units in them, like % and px, em. Initially, the rating value is a float, which is a <number></number> type. Using this conversion helps ensure we can use the values in a number of ways.

Filling the stars may sound tough, but turns out to be quite simple. We need a linear-gradient background to create hard color stops where the gold-colored fill should end:

background: linear-gradient(90deg,
  var(--star-background) var(--percent), 
  var(--star-color) var(--percent)
);

Note that I am using custom variables for colors because I want the styles to be easily adjustable. Because custom properties are inherited from the parent elements styles, you can define them once on the :root element and then override in an element wrapper. Here’s what I put in the root:

:root {
  --star-size: 60px;
  --star-color: #fff;
  --star-background: #fc0;
}

The last thing I did was clip the background to the shape of the text so that the background gradient takes the shape of the stars. Think of the Unicode stars as stencils that we use to cut out the shape of stars from the background color. Or like a cookie cutters in the shape of stars that are mashed right into the dough:

-webkit-background-clip: text;
-webkit-text-fill-color: transparent;

The browser support for background clipping and text fills is pretty darn good. IE11 is the only holdout.

Accessibility: ★★★★★
Management: ★★☆☆☆
Performance: ★★★★★
Maintenance: ★★★★★
Overall: ★★★★★

Final thoughts

Image Files Background Image SVG CSS Shapes Unicode Symbols
Accessibility ★★☆☆☆ ★★★☆☆ ★★★★★ ★★★★★ ★★★★★
Management ★★★★☆ ★★★★☆ ★★☆☆☆ ★★☆☆☆ ★★☆☆☆
Performance ★☆☆☆☆ ★★☆☆☆ ★★★★★ ★★★★★ ★★★★★
Maintenance ★★★★☆ ★★★☆☆ ★★★★☆ ★★☆☆☆ ★★★★★
Overall ★★☆☆☆ ★★★☆☆ ★★★★☆ ★★★☆☆ ★★★★★

Of the five methods we covered, two are my favorites: using SVG (Method 3) and using Unicode characters in pseudo-elements (Method 5). There are definitely use cases where a background image makes a lot of sense, but that seems best evaluated case-by-case as opposed to a go-to solution.

You have to always consider all the benefits and downsides of a specific method. This is, in my opinion, is the beauty of front-end development! There are multiple ways to go, and proper experience is required to implement features efficiently.