Turn a number into star rating display using jQuery and CSS
Solution 1:
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
(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
(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));
});
}
Solution 2:
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:
- You need the stars inside the element to generate correct width;
- There's no semantic markup, e.g. you'd prefer the score as text inside the element;
- 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.