Lean and accessible ratings with Drupal

Simple and accessible star ratings in Drupal where Fivestar is just simply overkill

Star rating widgets are a great and simple visual representations of two numbers: X out of Y. Drupal has support for those types of fields through contributed modules, the most popular being the excellent Fivestar module. However, using Fivestar could be just too much to display a static static value as a rating.

Often there is no need for user entered values, e.g when the value is set by editorial, frequently found on review content pages. And when working on a project with rating widgets I was baffled to see having such a fully-fledged module for ratings hurts performance, adds lots of redundant markup (divitis!) and there are extra CSS and JS files loaded automatically, not to mention the abuse of the form API.

For these types of applications I recommend using a simple number field accepting decimal values. It can still be made look pretty with some thoughtful styling and using a custom field formatter. Here is how I did it.

Create the field

I started with adding a field of ‘Decimal’ type and named it "Rating". The machine name of the field became field_rating. I wanted a rating widget with values ranging from 1 to 5 so I specified 1 as minimum value and 5 as maximum. I left precision at the default value (10) and set Scale to 1 as excessive granularity is not needed here.

It was time to start coding so I created a custom module and named it 'my_star_rating’.

Specify the theme function and template

We start by shaping the desired markup. The easiest option is to specify a template, providing the most flexible approach. The theme function should specify the rating value only. This comes straight from the value of the field.

/**
 * Implements hook_theme().
 */
function my_star_rating_theme($existing, $type, $theme, $path) {
  return array(
    'my_star_rating' => array(
      'path' => $path',
      'template' => 'star-rating',
      'variables' => array('rating' => NULL),
    ),
  );
}

And then the template with the desired markup, named star-rating.tpl.php:

<?php
/**
 * @file
 * Template for the star rating markup
 */
?>
<div class="stars-wrapper">
  <div class="stars-off stars">
    <div class="stars-on stars" style="width:<?php print (float) $rating / (int) 5 * 100; ?>%"></div>
  </div>
</div>
<span class="star-score"><strong><?php print $rating; ?></strong> / 5</span>

Add the field formatter

It is time to add the custom field formatter. This is done by implementing two field formatter hooks:

/**
 * Implements hook_field_formatter_info().
 */
function my_star_rating_field_formatter_info() {
  return array(
    'my_star_rating_formatter' => array(
      'label' => t('Star Rating'),
      'field types' => array('number_decimal')
    ),
  );
}

/**
 * Implements hook_field_formatter_view().
 */
function my_star_rating_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $element = array();

  foreach ($items as $delta => $item) {
    $element[$delta]['#markup'] = theme('my_star_rating', array(
      'rating' => round($item['value'], 1),
    ));
  }

  return $element;
}

That is, hook_formatter_info lets us specify our custom field formatter and hook_field_formatter_view lets us control the output of the field. In $element[$delta] we call a theme function instead of returning a full renderable array, feel free to customise it if full render arrays are preferred.

Configuring the display of the content type to show the rating field and use the provided field formatter results in the following markup when the field value is "3.7":

<div class="stars-wrapper">
  <div class="stars-off stars">
    <div class="stars-on stars" style="width:74%"></div>
  </div>
</div>
<span class="star-score"><strong>3.2</strong> / 5</span>

Much much less divitis here. Still a few extra elements but this probably is the bare minimum required to achieve this.

Styling with CSS

For the actual stars I wanted to use something scalable which works well across displays with different resolution. Font icons seem a sensible approach here so I extended my Fontello font icon set with a star glyph. The downloaded font had the star assigned to the value E80A in the mapping so I used this in my SASS styles:

.stars-wrapper {
  font-size: 2em; // This sets the size of the star rating widget.
  letter-spacing: -0.1em;
  display: inline;
  > .stars,
  > .stars > .stars {
    position: relative;
    white-space: nowrap;
    &:before {
      font: 1em/1 "fontello" normal normal;
      text-decoration: inherit;
      text-align: center;
      font-variant: normal;
      text-transform: none;
      speak: none;
      display: inline-block;
      content: "\E80A\E80A\E80A\E80A\E80A";
      color: grey; // This sets the colour of the 'empty' stars.
    }
    &.stars-on {
      position: absolute;
      top: 0;
      left: 0;
      overflow: hidden;
      &:before {
        color: red; // This sets the colour of the 'active' stars.
      }
    }
  }
}

So the actual formatted widget renders like this:

Rendered star rating in the browser

This renders beautifully on higher pixel density screens, it is freely scalable and more accessible as the star markup consists of empty elements but the rating itself is displayed so a screen reader would ideally have no problems with it.

Further improvements

Now what we have here is only the core of the idea—this can easily be improved further. For example there may be a requirement to have best values configurable, e.g. having ratings N out of 10 instead of 5.

Also, the markup could be extended with RDFa or microformat data to generate rich snippets in search results. This would involve setting up a preprocess function and a process function where all the properties of the review type definitions could be added using drupal_attributes().

Last updated on by Attila