blog

Star rating with decimal values in Preact

At Hugo we use Trustpilot to collect customer reviews. Since we get good reviews we wanted to show them off on our landing page.

Screenshot of Hugo Insurance landing page with Trustpilot rating

Using SVG to render multiple stars was an obvious choice. The crucial part was supporting decimal ratings so that decimal value is reflected on the amount of colored background.

Both Ahmad Shadeed and Samuel Kraft have great articles describing their techniques. But I did my own little twist since the design didn’t require changing the color of the star but the background instead.

Preact

First, here is my StarRating component:

import { h, Fragment } from 'preact';
import classnames from 'classnames';

const StarRating = ({ rating, starCount = 5 }) => (
<Fragment>
{
[ ...Array(starCount) ].map((_, i) => (
<div
key={ i }
className={ classnames('c-star-rating', {
'is-full': (i + 1) <= Math.floor(rating),
'is-decimal': (i + 1) === Math.ceil(rating),
}) }

style={
(i + 1) === Math.ceil(rating) &&
{ '--decimal': parseInt(((rating % 1) * 100).toFixed(0), 10) }
}

/>

))
}
</Fragment>
);

export default StarRating;

It will render a set number of stars (default being 5) representing max. rating possible and, depending on actual rating passed, set some CSS classes and custom properties.

className={ classnames('c-star-rating', {
'is-full': (i + 1) <= Math.floor(rating),
'is-decimal': (i + 1) === Math.ceil(rating),
}) }

Here I’m using classnames utility to conditionally set modifier classes along my base c-star-rating class. is-full is added on all stars fully colored (ie. for 2.4 rating, the first two stars will have is-full class). Star that needs to be only partially filled (where decimal value stops) will have is-decimal value.

style={
(i + 1) === Math.ceil(rating) &&
{ '--decimal': parseInt(((rating % 1) * 100).toFixed(0), 10) }
}

Next, on that same "decimal" star I set --decimal custom property holding the remaining value but converted in the percentage of 100 (so 2.4 becomes 40). I’m using logical AND (&&) to short-circuit evaluation instead of having ternary expression.

SVG and Sass

My _star-rating.scss partial looks something like this:

// White star for the rating
// svg-url function comes from here: https://codepen.io/jakob-e/pen/doMoML
$star: svg-url('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="#fff" d="M15.524 6.464a.5.5 0 0 1 .952 0l1.881 5.791a.5.5 0 0 0 .476.346h6.09a.5.5 0 0 1 .294.904l-4.927 3.58a.5.5 0 0 0-.182.559l1.882 5.792a.5.5 0 0 1-.77.559l-4.926-3.58a.5.5 0 0 0-.588 0l-4.927 3.58a.5.5 0 0 1-.77-.56l1.883-5.791a.5.5 0 0 0-.182-.56l-4.927-3.579a.5.5 0 0 1 .294-.904h6.09a.5.5 0 0 0 .476-.346l1.881-5.791Z"/></svg>');

.c-star-rating {
width: 30px;
height: 30px;
display: inline-flex;
position: relative;
border-radius: 3px;
background-color: #DCDCE6;
overflow: hidden;
margin: 0 2px;

&:after {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
content: "";
background-image: $star;
background-position: center;
background-repeat: no-repeat;
background-size: 100%;
}

&.is-full {
background-color: #ED56A1;
}

&.is-decimal {
&:before {
position: absolute;
top: 0; left: 0;
content: "";
background-color: #ED56A1;
width: calc(var(--decimal, 0) * 1%);
height: 100%;
}
}
}

This particular project doesn’t have a good icon system in place so instead of adding SVG directly in JSX I opted for SVG-in-CSS approach and set encoded SVG as background-image. Since star doesn't change color almost any SVG approach could work here: using img, external SVG linked in url, SVG-in-JSX or SVG as a symbol.

By setting the star as the background image in after pseudo-element, I made sure that stays above the square that will change color.

Partial coloring of the square is achieved by using before and passed --decimal value to set its width.

width: calc(var(--decimal, 0) * 1%);

Since --decimal value is unitless, I use calc and multiply it by 1% to get the percentage value.

Final result

See the Pen Untitled by Teo Dragovic (@teodragovic) on CodePen.