Fractional SVG stars with CSS

Fractional SVG stars with CSS

Building a stars/rating component and need to support fractional values like 4.2 or 3.7 but don't want to use images? This post explains how to make fractional stars using just CSS and inline SVGs.

I was working on displaying reviews on an ecommerce site when needing a component displaying the customers ratings. The previous version of the website used multiple png images overlayed on top of each other, contributing to unnessecary requests and CLS issues. The criteria for the new component was:

  • Should use inline SVG:s instead of images
  • The number of stars should be dynamic
  • Should support fractional values

Final component

Here's the component we are building:

Test the component by changing the values above 馃憜

The code:

import IconStar from 'star.svg';

const Rating = ({ value, max, className }) => {
	/* Calculate how much of the stars should be "filled" */
  const percentage = Math.round((value / max) * 100);

  return (
    <div className={styles.container}>
    {
      /* Create an array based on the max rating, render a star for each */
    }
      {Array.from(Array(max).keys()).map((_, i) => (
        <IconStar key={i} className={styles.star} />
      ))}
    {
      /* Render a div overlayed on top of the stars that should not be not filled */
    }
      <div className={styles.overlay} style={{ width: `${100 - percentage}%` }} />
    </div>
  );
}

The component basically consists of two parts:

  1. A list of star icons based on the max rating (max prop)
  2. An "overlay" div that will be responsible for changing the color of the stars underneath. This is the magic that makes the fractional part work.

The overlay is just a plain div the same size as the part of the stars that should be a different color/unfilled. We calculate the width of the div by first dividing the rating with the maximum, then subtracting that value from 100.

const percentage = Math.round((value / max) * 100);

<div className={styles.overlay} style={{ width: `${100 - percentage}%` }} />

Add the styles below in order to lay out the stars:

.container {
  display: inline-flex;
  align-items: center;
  position: relative;
}

.star {
  width: 18px;
  margin-right: 2px;
  display: flex;
  color: #f8d448;

  &:last-of-type {
    margin-right: 0;
  }
}

.overlay {
  background-color: black;
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
}

Now the overlay is just plain black though, lets change that:

Using mix-blend-mode to change SVG color

The final step is making the overlay div only affect the star SVGs beneath, not the background. We can do this by using the CSS mix-blend-mode property with the color value.

The color spec reads as following:

Creates a color with the hue and saturation of the source color and the luminosity of the backdrop color. This preserves the gray levels of the backdrop and is useful for coloring monochrome images or tinting color images.

Tinting color images is exactly what we want to do, so let's add the property and see what happens:

.overlay {
  background-color: black;
  mix-blend-mode: color;
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
}

This is exactly what we want! It changes the hue and saturation of the stars, but keeps the backgrund color unchanged. You can play with the background-color to change the tint of the stars. For example if I use background-color: red instead I get a red tint instead of gray stars.

The browser support is pretty good (supported in all modern browsers) but for older browsers we can fall back to opacity instead. We can do this by setting the default styles to the fallback, and progressively adding the mix-blend-mode if the browser supports it:

Update: Thanks to reader Chris Morgan for pointing out that we need to reverse our properties and start with the fallback, as all browsers that don鈥檛 support mix-blend-mode also don鈥檛 support @supports.

.overlay {
  background-color: black;
  opacity: 0.7;
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 1;

  @supports (mix-blend-mode: color) {
    mix-blend-mode: color;
    opacity: unset;
  }
}

You can find the full source code here. Let me know if you found this CSS tip useful by pressing the heart below 馃憞

Related Posts