106

I have been looking at jquery plugin and was wondering how to adapt that plugin to turn a number (like 4.8618164) into a 4.8618164 stars filled out of 5. Basically interpreting a number <5 into stars filled in a 5-star rating system using jQuery/JS/CSS.

Note that this would only display/show the stars rating from an already available number and not accept new ratings submissions.

madth3
  • 7,275
  • 12
  • 50
  • 74
Steve
  • 4,946
  • 12
  • 45
  • 62
  • this is a [wonderful and easy to use plugin](https://github.com/wbotelhos/raty) – wpcoder Nov 09 '17 at 01:17
  • This one is pure css and doesn't require javascript for the star display (but you may need javascript to set the value dynamically) https://codepen.io/jamesbarnett/pen/vlpkh – User Dec 23 '20 at 12:39

9 Answers9

258

Here's a solution for you, using only one very tiny and simple image and one automatically generated span element:

CSS

span.stars, span.stars span {
    display: block;
    background: url(stars.png) 0 -16px repeat-x;
    width: 80px;
    height: 16px;
}

span.stars span {
    background-position: 0 0;
}

Image

alt text
(source: ulmanen.fi)

Note: do NOT hotlink to the above image! Copy the file to your own server and use it from there.

jQuery

$.fn.stars = function() {
    return $(this).each(function() {
        // Get the value
        var val = parseFloat($(this).html());
        // Make sure that the value is in 0 - 5 range, multiply to get width
        var size = Math.max(0, (Math.min(5, val))) * 16;
        // Create stars holder
        var $span = $('<span />').width(size);
        // Replace the numerical value with stars
        $(this).html($span);
    });
}

If you want to restrict the stars to only half or quarter star sizes, add one of these rows before the var size row:

val = Math.round(val * 4) / 4; /* To round to nearest quarter */
val = Math.round(val * 2) / 2; /* To round to nearest half */

HTML

<span class="stars">4.8618164</span>
<span class="stars">2.6545344</span>
<span class="stars">0.5355</span>
<span class="stars">8</span>

Usage

$(function() {
    $('span.stars').stars();
});

Output

Image from fugue icon set (www.pinvoke.com)
(source: ulmanen.fi)

Demo

http://www.ulmanen.fi/stuff/stars.php

This will probably suit your needs. With this method you don't have to calculate any three quarter or whatnot star widths, just give it a float and it'll give you your stars.


A small explanation on how the stars are presented might be in order.

The script creates two block level span elements. Both of the spans initally get a size of 80px * 16px and a background image stars.png. The spans are nested, so that the structure of the spans looks like this:

<span class="stars">
    <span></span>
</span>

The outer span gets a background-position of 0 -16px. That makes the gray stars in the outer span visible. As the outer span has height of 16px and repeat-x, it will only show 5 gray stars.

The inner span on the other hand has a background-position of 0 0 which makes only the yellow stars visible.

This would of course work with two separate imagefiles, star_yellow.png and star_gray.png. But as the stars have a fixed height, we can easily combine them into one image. This utilizes the CSS sprite technique.

Now, as the spans are nested, they are automatically overlayed over each other. In the default case, when the width of both spans is 80px, the yellow stars completely obscure the grey stars.

But when we adjust the width of the inner span, the width of the yellow stars decreases, revealing the gray stars.

Accessibility-wise, it would have been wiser to leave the float number inside the inner span and hide it with text-indent: -9999px, so that people with CSS turned off would at least see the floating point number instead of the stars.

Hopefully that made some sense.


Updated 2010/10/22

Now even more compact and harder to understand! Can also be squeezed down to a one liner:

$.fn.stars = function() {
    return $(this).each(function() {
        $(this).html($('<span />').width(Math.max(0, (Math.min(5, parseFloat($(this).html())))) * 16));
    });
}
Glorfindel
  • 21,988
  • 13
  • 81
  • 109
Tatu Ulmanen
  • 123,288
  • 34
  • 187
  • 185
  • @Tatu, any chance of fleshing it out a bit more with *how* this works. My knowledge of jQuery is just a tad short to totally get it. I think I understand how the CSS works with repeating in the x direction and the 16*5=80 bit and how you adjust the width of the yellow-star image, but how are you overlaying the two images on top of each other from a single image that has one star above the other? Does the -16px bring the gray star up to the same level as the yellow? – paxdiablo Jan 01 '10 at 03:42
  • Thanks for the update, @Tatu, that makes a lot more sense to me now. Unfortunately I'd already given you a +1 so I can't do it again but hopefully the explanation will earn you some more (well-earned) rep. Cheers. – paxdiablo Jan 01 '10 at 13:43
  • 2
    @paxdiablo and others, thanks for the support. Perhaps I should make this a proper jQuery plugin as this technique seems to come as a surprise to most :) – Tatu Ulmanen Jan 01 '10 at 14:41
  • @Tatu, thanks immensely. The existing five star plugins all seem to want a form which is fine for inputing a rating, but way overkill to change the representation of a number. – vfilby Apr 12 '10 at 19:22
  • Tatu, I'm using your stars plugin inside ajax tooltips generated by the qTip jQuery plugin. In the beggining it works well, but as you mouse over a few links and get the tooltips, it appears the width of the yellow stars layer does not get reset, therefor something with a rating of 3 will show five gold stars. Is there a way to reset the width of the yellow layer after the tooltip closes? – Mohamad May 31 '10 at 21:09
  • @Mel, an easy solutions seems to be to run this snippet every time you open the tooltip: `$('.tooltip .stars').html(4.5354).stars();`. That will destroy the current stars, populate the container with a new value and then turn that value into stars. – Tatu Ulmanen Jun 01 '10 at 06:45
  • Tatu, thanks. I was wrong: I have five links and each creates a tooltip. As you mouseover from first to last, each tooltip is created and the stars display correctly. But as each new tooltip is created, it erases the width value for all the other previously created tooltips: the contains no width value. Moving back to a previous tooltip does not restore the width value. I tried your suggestion, but I don't think I'm doing it right. My code: $(this).qtip({ content: { url: 'data.cfc' }, api :{ onContentUpdate : function(){ //stars $('span.stars').stars();} – Mohamad Jun 01 '10 at 15:13
  • @Tatu Ulmanen: Definitely worth a +1 and much more! I'm looking for something like that, but I also need to be able to accept new rating submissions. Did you turn that into a full plugin yet? If not, would you please give a hint of how to accept rating submissions? Thanks a bunch! :) – Kassem Mar 28 '11 at 20:56
  • @iSun, just change display: block; to display: inline-block; – Tatu Ulmanen Jul 04 '13 at 20:18
  • Apologies for hotlinking. I wasn't thinking. I have revised my JSBin's to use this link instead: https://dl.dropboxusercontent.com/u/2176587/Assets/star.png – coderberry Aug 12 '13 at 15:11
  • You can specify a percent width on the inner span e.g. `.css('width', Math.round(4.1 / 5 * 100) + '%') /* 82% */`. It is easier IMO. – Salman A Nov 09 '13 at 16:36
  • 8
    **Or even smaller:** http://jsbin.com/IBIDalEn/2/edit (P.S. removed unneeded stuff in JS, minified CSS selectors and used `max-width`). – Roko C. Buljan Dec 13 '13 at 04:06
  • +1 for your great solution and brilliant explenation. I know this post is old. but I have a question. Is there any workaround to make the Star Images responsive(without using MediaQuery)? I want the images to get smaller when they are placed into a small div. – Mehrdad Kamelzadeh May 30 '16 at 15:48
  • This is fantastic!! Helped me tremendously! Quick note for anyone who sees this in the future. If you're already using ajax, make sure this script is using the same most recent ajax plugin otherwise it won't work. I was able to use this to show the rating for 275+ items from a database. Thank you so much @TatuUlmanen !!!! – Steve C. Feb 03 '17 at 03:59
  • Thanks. It's working fine. But some time it may create problem in angularjs during render data. – Hermenpreet Singh Mar 28 '18 at 05:35
40

Anno 2022 there's more new methods available for modern browsers that require less runtime code if you can generate the rating/percentage in the right location.

Have your framework generate this html with e.g. 33.333% in the right style spots and you're good to go:

.star-rating::before {
  /* What also works: "⭐⭐⭐⭐⭐" or "" or other characters. */
  content: "⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐";
}

.star-rating {
    display: inline-block;
    background-clip: text;
    -webkit-background-clip: text;
    color: rgba(0, 0, 0, 0.1);
}

body {
  font-size: 32px; /* for demo purpose */
}
<div
  class="star-rating"
  style="background-image: linear-gradient(
      to right,
      gold 0%,
      gold 33.333%,
      transparent 33.333%,
      transparent 100%);
  "
><!-- Don't forget to add an accessible alternative! --></div>

The original answer that accrued the first 40 upvotes then too:

If you only have to support modern browsers, you can get away with:

  • No images;
  • Mostly static css;
  • Nearly no jQuery or Javascript;

You only need to convert the number to a class, e.g. class='stars-score-50'.

First a demo of "rendered" markup:

body { font-size: 18px; }

.stars-container {
  position: relative;
  display: inline-block;
  color: transparent;
}

.stars-container:before {
  position: absolute;
  top: 0;
  left: 0;
  content: '★★★★★';
  color: lightgray;
}

.stars-container:after {
  position: absolute;
  top: 0;
  left: 0;
  content: '★★★★★';
  color: gold;
  overflow: hidden;
}

.stars-0:after { width: 0%; }
.stars-10:after { width: 10%; }
.stars-20:after { width: 20%; }
.stars-30:after { width: 30%; }
.stars-40:after { width: 40%; }
.stars-50:after { width: 50%; }
.stars-60:after { width: 60%; }
.stars-70:after { width: 70%; }
.stars-80:after { width: 80%; }
.stars-90:after { width: 90%; }
.stars-100:after { width: 100; }
Within block level elements:

<div><span class="stars-container stars-0">★★★★★</span></div>
<div><span class="stars-container stars-10">★★★★★</span></div>
<div><span class="stars-container stars-20">★★★★★</span></div>
<div><span class="stars-container stars-30">★★★★★</span></div>
<div><span class="stars-container stars-40">★★★★★</span></div>
<div><span class="stars-container stars-50">★★★★★</span></div>
<div><span class="stars-container stars-60">★★★★★</span></div>
<div><span class="stars-container stars-70">★★★★★</span></div>
<div><span class="stars-container stars-80">★★★★★</span></div>
<div><span class="stars-container stars-90">★★★★★</span></div>
<div><span class="stars-container stars-100">★★★★★</span></div>

<p>Or use it in a sentence: <span class="stars-container stars-70">★★★★★</span> (cool, huh?).</p>

Then a demo that uses a wee bit of code:

$(function() {
  function addScore(score, $domElement) {
    $("<span class='stars-container'>")
      .addClass("stars-" + score.toString())
      .text("★★★★★")
      .appendTo($domElement);
  }

  addScore(70, $("#fixture"));
});
body { font-size: 18px; }

.stars-container {
  position: relative;
  display: inline-block;
  color: transparent;
}

.stars-container:before {
  position: absolute;
  top: 0;
  left: 0;
  content: '★★★★★';
  color: lightgray;
}

.stars-container:after {
  position: absolute;
  top: 0;
  left: 0;
  content: '★★★★★';
  color: gold;
  overflow: hidden;
}

.stars-0:after { width: 0%; }
.stars-10:after { width: 10%; }
.stars-20:after { width: 20%; }
.stars-30:after { width: 30%; }
.stars-40:after { width: 40%; }
.stars-50:after { width: 50%; }
.stars-60:after { width: 60%; }
.stars-70:after { width: 70%; }
.stars-80:after { width: 80%; }
.stars-90:after { width: 90%; }
.stars-100:after { width: 100; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

Generated: <div id="fixture"></div>

The biggest downsides of this solution are:

  1. You need the stars inside the element to generate correct width;
  2. There's no semantic markup, e.g. you'd prefer the score as text inside the element;
  3. It only allows for as many scores as you'll have classes (because we can't use Javascript to set a precise width on a pseudo-element).

To fix this the solution above can be easily tweaked. The :before and :after bits need to become actual elements in the DOM (so we need some JS for that).

The latter is left as an excercise for the reader.

Jeroen
  • 60,696
  • 40
  • 206
  • 339
26

Try this jquery helper function/file

jquery.Rating.js

//ES5
$.fn.stars = function() {
    return $(this).each(function() {
        var rating = $(this).data("rating");
        var fullStar = new Array(Math.floor(rating + 1)).join('<i class="fas fa-star"></i>');
        var halfStar = ((rating%1) !== 0) ? '<i class="fas fa-star-half-alt"></i>': '';
        var noStar = new Array(Math.floor($(this).data("numStars") + 1 - rating)).join('<i class="far fa-star"></i>');
        $(this).html(fullStar + halfStar + noStar);
    });
}

//ES6
$.fn.stars = function() {
    return $(this).each(function() {
        const rating = $(this).data("rating");
        const numStars = $(this).data("numStars");
        const fullStar = '<i class="fas fa-star"></i>'.repeat(Math.floor(rating));
        const halfStar = (rating%1!== 0) ? '<i class="fas fa-star-half-alt"></i>': '';
        const noStar = '<i class="far fa-star"></i>'.repeat(Math.floor(numStars-rating));
        $(this).html(`${fullStar}${halfStar}${noStar}`);
    });
}

index.html

   <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Star Rating</title>
        <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.9.0/css/all.min.css" rel="stylesheet">
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
        <script src="js/jquery.Rating.js"></script>
        <script>
            $(function(){
                $('.stars').stars();
            });
        </script>
    </head>
    <body>

        <span class="stars" data-rating="3.5" data-num-stars="5" ></span>

    </body>
    </html>

Screenshot

Rajasekar D
  • 450
  • 2
  • 11
  • 23
7

Why not just have five separate images of a star (empty, quarter-full, half-full, three-quarter-full and full) then just inject the images into your DOM depending on the truncated or rouded value of rating multiplied by 4 (to get a whole numner for the quarters)?

For example, 4.8618164 multiplied by 4 and rounded is 19 which would be four and three quarter stars.

Alternatively (if you're lazy like me), just have one image selected from 21 (0 stars through 5 stars in one-quarter increments) and select the single image based on the aforementioned value. Then it's just one calculation followed by an image change in the DOM (rather than trying to change five different images).

paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
5

I ended up going totally JS-free to avoid client-side render lag. To accomplish that, I generate HTML like this:

<span class="stars" title="{value as decimal}">
    <span style="width={value/5*100}%;"/>
</span>

To help with accessibility, I even add the raw rating value in the title attribute.

Tim Schmidt
  • 1,855
  • 3
  • 21
  • 21
4

DEMO

You can do it with 2 images only. 1 blank stars, 1 filled stars.

Overlay filled image on the top of the other one. and convert rating number into percentage and use it as width of fillter image.

enter image description here

.containerdiv {
  border: 0;
  float: left;
  position: relative;
  width: 300px;
} 
.cornerimage {
  border: 0;
  position: absolute;
  top: 0;
  left: 0;
  overflow: hidden;
 } 
 img{
   max-width: 300px;
 }
Elyor
  • 5,396
  • 8
  • 48
  • 76
3

using jquery without prototype, update the js code to

$( ".stars" ).each(function() { 
    // Get the value
    var val = $(this).data("rating");
    // Make sure that the value is in 0 - 5 range, multiply to get width
    var size = Math.max(0, (Math.min(5, val))) * 16;
    // Create stars holder
    var $span = $('<span />').width(size);
    // Replace the numerical value with stars
    $(this).html($span);
});

I also added a data attribute by the name of data-rating in the span.

<span class="stars" data-rating="4" ></span>
Galuga
  • 531
  • 2
  • 6
1

Here's my take using JSX and font awesome, limited on only .5 accuracy, though:

       <span>
          {Array(Math.floor(rating)).fill(<i className="fa fa-star"></i>)}
          {(rating) - Math.floor(rating)==0 ? ('') : (<i className="fa fa-star-half"></i>)}
       </span>

First row is for whole star and second row is for half star (if any)

Ardhi
  • 2,855
  • 1
  • 22
  • 31
1

Stars rating using CSS

  • background-clip to paint the star characters with a linear-gradient background
  • CSS var() to pass the rating value from the template to the gradient image percentage

[style^=--rating]::after {
  content: "★★★★★";
  font-size: 2em;
  white-space: nowrap;
  background: linear-gradient(90deg, #fb0 calc(var(--rating) * 20%), #ddd calc(var(--rating) * 20%));
  background-clip: text;
  -webkit-background-clip: text;
  color: #0000;
}
<span style="--rating:5.0"></span> <br>
<span style="--rating:1.7"></span> <br>
<span style="--rating:3.5"></span> <br>

For accessibility you can also add the Aria attribute like aria-label="Rating: 4.5 stars".


Stars rating using CSS and JavaScript

Whilst the above example is great for rating preview purpose, if you want to allow the user to cast a vote, you'll need to add some JavaScript for handling click, UX colors change on pointer interaction, etc. — See this related answer:
Star rating with pointer events

Roko C. Buljan
  • 196,159
  • 39
  • 305
  • 313