How to make a star rating display in React that's better than the one on yelp.com

Display average star rating to a precise degree with JavaScript's linear gradient background

Featured on Hashnode

This post will discuss how to create a star rating display in React, and a star rating picker with color-changing hover effects similar to Yelp.com's version.

Yelp's star rating display rounds to the half star:

yelpfourthree.jpg

yelpfourtwo.jpg

Yelp has over 5 million claimed businesses. Assuming an even distribution of average star ratings, rounding down from .2 and up from .8 suggests Yelp has over 2 million worth of fractions of stars either taken away or falsely awarded.

The version we will build here rounds to the tenth. It could go even further than this, but it wouldn't make a difference visually:

starratingfraction.jpg

Star Rating Display

Overview

  • Get the average star rating. This will be converted to an array.
  • Calculate the number of full stars, the decimal/fraction of the partial star, and the number empty stars
  • Push them into an array of 5, with 1s for full stars, a decimal for the partially filled star (like 0.3), and 0s for empty stars
  • Map the array into divs and set the background color to a linear gradient that changes from your fill color (orange here), to an empty color (gray), based on the values in the array
  • The 1s in the array become 100% full of the fill color, the partial star becomes partially full of the fill color based on the decimal, and the empty stars are 0% full of the fill color and only colored with the empty color

Code

const fullStars = Math.floor(starAverage);
  // Gets the number of full stars. starAverage is the rating, for example 
  // if the rating were 4.3, fullStars would now be 4.

const starArr = [];
  // Create an empty array. We will add 1s, 0s, and a decimal value for the 
  // partial star.

for(let i = 1; i <= fullStars; i++)
{
  starArr.push(1);
}
  // This adds a 1 to the array for each full star in our rating

if(starAverage < 5) {
  // Wrapped in an if block because the following only needs to occur if 
  // it's not a full 5.

  const partialStar = starAverage - fullStars;
    // Calculates the partial star. For example 4.3 - 4 = 0.3. 0.3 will get 
    // added to the array in the next line to represent the partial star

  starArr.push(partialStar);
    // Adds the partial star to the array

  const emptyStars = 5 - starArr.length;
    // Calculates the number of empty stars

  for(let i=1; i<=emptyStars; i++) {
    starArr.push(0);
  }
    // This for loop adds 0s to the array to represent empty stars
}

const stars = starArr.map((val, i) => {
  return <div key={i} 
    className={style.starBox} 
    style={{background: `linear-gradient(90deg, #ff643d 
    ${val * 100}%, #bbbac0 ${val * 100}%)`}}></div>
  })
  // This last block is explained in the following paragraphs below

The last block starting with const stars = takes the array of 1s, a decimal, and zeros we created and maps it to 5 div elements. The stars variable is then used later in the return statement of the React component where we want to show the star rating. The key to getting the color fill right is the style attribute.

style={{background: `linear-gradient(90deg, #ff643d ${val * 100}%, #bbbac0 ${val * 100}%)`}}>★</div> is setting the background color of each div using a linear gradient.

The first argument, 90deg, means the gradient color change is happening from left to right across the div. The next argument, #ff643d ${val * 100}%, is saying we start at the color orange, and continue right with solid orange until a certain percentage across. The percentage is calculated from val, which is the 1, decimal or zero coming from the starArr array. So if it is a 1, it colors orange 100% across the div for a full star. For a decimal, like 0.3, it will color orange 30% across the div.

The next argument, #bbbac0 ${val * 100}, is another color stop using gray, and the same calculated percentage. This just means that there is no smooth transition between colors because the percent value is the same as the last color stop, so there is an immediate change from orange to gray. Read the MDN linear gradient docs to learn more.

If the value getting mapped is a 0, then there will be no orange in the background of the div and only gray, because the switch from orange to gray happens at 0% of the way across the linear gradient.

Star Rating Picker

pickergif.gif

This is built in a similar way with some changes. The entire react component with commented explanations is below:

import React, { useEffect } from 'react';
import style from './StarRatingPicker.module.css';

export default function StarRatingPicker({ rate, rating, changeColor, color, 
  parent }) {

  const colors = ['#FFD56A', '#FFA448', '#ff7e42', '#ff523d', '#f43939'];
    // An array of colors going from yellow to dark red

  useEffect(() => {
    const i = colors[Math.floor(rating - 1)];
    changeColor(i)
  }, [])
    // This sets the initial color to state based on any initial rating.

  function hoverRating() {
    if (rating === 0) {
        return "Select your rating"
    }
    else if (rating === 1) {
        return "Not good"
    }
    else if (rating === 2) {
        return "Could've been better"
    }
    else if (rating === 3) {
        return "OK"
    }
    else if (rating === 4) {
        return "Good"
    }
    else if (rating === 5) {
        return "Great"
    }
  }
    // This provides text that goes with the different ratings

  const fullStars = Math.floor(rating);
    // Again calculating the number of full stars

  const starArr = [];

  for(let i = 1; i <= fullStars; i++)
  {
    starArr.push(1);
  }
    // Adding 1s to the starArr array to represent full stars

    if(starAverage < 5) {
      const partialStar = starAverage - fullStars;
      starArr.push(partialStar);
        // Calculate and add the partial star to the array as a decimal value

    const emptyStars = 5 - starArr.length;
      for(let i=1; i<=emptyStars; i++) {
      starArr.push(0);
        // Calculate and add the empty stars to the array as zeros
      }
    }

  const starRatingPicker = starArr.map((val, index) => {
    return <div key={index}
      className={style.starBox}
      onClick={() => rate(index + 1)}
      onMouseEnter={() => {
        rate(index + 1);
        changeColor(colors[index]);
      }
          // Again mapping the array to divs as in the previous example. 
          // Here we add an onClick listener to call a rate function that 
          // sets a rating state, which is used in a post to the server to 
          // create the actual rating.
          // ChangeColor sets a color state from the colors array at the 
          // top of the component, which will be used below
    } 
    style={{background: `linear-gradient(90deg, ${color}, ${color} 
      ${val * 100}%, #bbbac0 ${val * 100}%)`}}>★</div>
        // Setting the background color of the div. This is the same as 
        // the previous example, except instead of a hard-coded orange, 
        // the color value from state is used
  })

  return <div className={style.rating + ' ' + parent}>{starRatingPicker}
    <p className={style.hoverText}>{hoverRating()}</p>
    </div>
}