Developing an Accessible Star Ratings Widget

This article was first published on yuiblog.com (08-24-2010).

In a hurry? Skip to the demo page

Many ecommerce sites, social networking services, and online communities include rating or assessment features. Soliciting people’s opinion has even become a business model; there are now sites dedicated to rating products, services, businesses, and more.

The most common interface used to display votes is the “star rating system,” in which a particular number of points (often expressed as stars) is assigned to an item by each reviewer. We find this model on many sites, from Amazon to Yelp.

Examples of star rating systems Figure A. Star rating examples from Amazon and Yelp

As Figure A shows, both visual interfaces are similar, but what makes these two solutions interesting is their markup base. One relies on <map>, the other on <img>.

You might think that most rating systems would be based on some markup proven to be semantic and “operational” across many User Agents — that is, that rating systems would be based on a specific set of HTML elements and attributes to which one applies behavior and style via JS and CSS. That would make sense, but it is far from the truth. When it comes to markup, authors try just about everything:

The case of Microformats

Before presenting a few image-based techniques to mark up ratings, I think it is worth mentioning a basic and straightforward approach (from Microformats) that uses characters:

<abbr class="rating" title="3 stars">***</abbr>
Pros
It is straightforward and semantic.
The markup is minimal.
The method is not reliant on CSS.
The method is not reliant on images.
There is no HTTP request.
Cons
It is impossible to represent half values (i.e. 3.5 stars)
It “works” only with asterisks (“star rating”)
Screen-readers, by default, do not expand abbreviations (which may not be a big deal in this case)

(Note: I use “*” rather than ★ (&#9733;) because screen-readers (at least JAWS and NVDA) seem to ignore html entities.)

Markup to display image-based ratings

When it comes to display images, authors have many options.

One image per rating

Using a single image:

<img src="4stars.png" alt="4 out of five" />
One star
1 out of five
Two stars
2 out of five
Three stars
3 out of five
Four stars
4 out of five
Five stars
5 out of five

Pros and Cons of this method

Pros
Using one image per rating is straightforward and semantic.
The method is not reliant on CSS.
Minimal markup.
Cons
It creates many HTTP requests as there are many different images.
On top of the performance issue, it can be a maintenance nightmare as authors have to deal with more assets (images to create, to push to a CDN, to modify when site colors change, etc.).
Text selection is not possible in Opera (at least in version 9.52) as the alternate text is ignored

One image per unit

From the whatwg’s working draft:

<img alt="4 out of 5" src="one-star.png">
<img alt="" src="one-star.png">
<img alt="" src="one-star.png">
<img alt="" src="one-star.png">
<img alt="" src="no-star.png">
One star
1 out of five
Two stars
2 out of five
Three stars
3 out of five
Four stars
4 out of five
Five stars
5 out of five

Pros and Cons of this method

Pros
Using two img elements per rating diminishes the number of HTTP requests.
The method is not reliant on CSS.
Cons
In Opera, when images are disabled, alternate text is not selectable, and (in small-screen view) that text is rendered with a border which makes it less legible.

(Note that this is taken from a controversial working draft. In my opinion, this method is not acceptable because the alternate text does not describe the image accurately and succinctly. Besides, if the basis of this approach is that these images represent content, then why leave some of them with no alt text?)

On Ajaxian, for example, the author is using alternate text with every single image, which makes a lot of sense if he considers that each one is content:

<img [snip] alt="+" src="star1.png"/>
<img [snip] alt="+" src="star1.png"/>
<img [snip] alt="+" src="star1.png"/>
<img [snip] alt="-" src="star0.png"/>
<img [snip] alt="-" src="star0.png"/>

In any case, using as many images as there are stars versus using a single element (an img or something else) has the main advantage of facilitating voting mechanisms - where a user selects one of the stars to cast his vote. So we should keep this in mind.

A sprite for background images

The following technique is a adaptation of a strategy originally implemented by developers at Yahoo! Music:

Markup

<span class="rating r1 stars">
    1 of 5
</span>
<span class="rating r2 stars">
    2 of 5
</span>
<span class="rating r3 stars">
    3 of 5
</span>
<span class="rating r4 stars">
    4 of 5
</span>
<span class="rating r5 stars">
    5 of 5
</span>

CSS

.stars {
  background:   transparent url(sprite.png)
                no-repeat;
}
.rating {
  font-size: 0;
  height: 19px;
  overflow: hidden;
  vertical-align: middle;
  width: 96px;
  display: block;
}
.r1 { background-position: -385px 0; }
.r2 { background-position: -288px 0; }
.r3 { background-position: -192px 0; }
.r4 { background-position:  -96px 0; }
One star
1 of 5
Two stars
2 of 5
Three stars
3 of 5
Four stars
4 of 5
Five stars
5 of 5

Pros and Cons

Pros
This method requires a single HTTP request as it relies on a single sprite image.
Minimal “foot print”.
Cons
Content is not revealed with images off.
Nothing shows when the page is printed (a print stylesheet could take care of this issue).
In Opera, the high contrast stylesheet makes all the stars disappear; the same is true in High Contrast Mode Optimization.
Text selection is possible, but it’s not obvious (via highlighting).

A sprite in the markup

This approach is based on the TIP method, which uses a sprite image as an <img> element rather than a background image:

Markup

<span   title="1 of 5"
        class="rating r1">
    <img    width="0"
            height="1"
            src="sprite.gif"
            alt=""/>
    1 out of 5
</span>
<span   title="2 of 5"
        class="rating r2">
    <img    width="0"
            height="1"
            src="sprite.gif" alt=""/>
    2 out of 5
</span>
<span   title="3 of 5"
        class="rating r3">
    <img    width="0"
            height="1"
            src="sprite.gif" alt=""/>
    3 out of 5
</span>
<span   title="4 of 5"
        class="rating r4">
    <img    width="0"
            height="1"
            src="sprite.gif" alt=""/>
    4 out of 5
</span>
<span   title="5 of 5"
        class="rating r5">
    <img    width="0"
            height="1"
            src="sprite.gif" alt=""/>
    5 out of 5
</span>

CSS

.rating {
    position: relative;
    height: 1.6em;
    width: 8.1em;
    overflow: hidden;
    vertical-align: middle;
    display: block;
}
.rating img {
    position: absolute;
    width: 40.5em;
    height: 1.55em;
    top: 0;
    border: 1px solid #fff;
}
.r1 img {
    right: 0;
}
.r2 img {
    left: -24.4em;
}
.r3 img {
    left: -16.2em;
}
.r4 img {
    left: -8.1em;
}
One star
1 out of 5
Two stars
2 out of 5
Three stars
3 out of 5
Four stars
4 out of 5
Five stars
5 out of 5

Pros and Cons

Pros
This method requires a single HTTP request.
This technique is the only one of the four methods above that reveals content when Firefox users select “hide images” or “make images invisible” (from the developer’s toolbar).
When images are unavailable a red “x” appears only in the highest rating (i.e. 5 out of 5) instead of in each one as it is the case with other solutions that rely on img elements.
Cons
The display of images is reliant on CSS.

It is worth noting that unlike other Image Replacement techniques, this method allows:

Markup to cast votes

Starting with a native mechanism

To cast votes, we need a low-level voting mechanism that allows simple user selection and submission. For this, we can rely on using a form with labels and controls:

Markup

<fieldset>
    <legend>Rating</legend>
    <label>
        <input  type="radio"
                name="movie"
                value="1_5"> 1/5
    </label>
    <label>
        <input  type="radio"
                name="movie"
                value="2_5"> 2/5
    </label>
    <label>
        <input  type="radio"
                name="movie"
                value="3_5"> 3/5
    </label>
    <label>
        <input  type="radio"
                name="movie"
                value="4_5"> 4/5
        </label>
    <label>
        <input  type="radio"
                name="movie"
                value="5_5"> 5/5
    </label>
</fieldset>

Result

Rating






Adding breaks and whitespace

For better legibility, we add <br> and whitespace.

Markup

<fieldset>
    <legend>Rating</legend>
    <label>
        <input  type="radio"
                name="movie"
                value="1_5"> 1/5
    </label><br>
    <label>
        <input  type="radio"
                name="movie"
                value="2_5"> 2/5
    </label><br>
    <label>
        <input  type="radio"
                name="movie"
                value="3_5"> 3/5
    </label><br>
    <label>
        <input  type="radio"
                name="movie"
                value="4_5"> 4/5
    </label><br>
    <label>
        <input  type="radio"
                name="movie"
                value="5_5"> 5/5
    </label>
</fieldset>

Result

Rating






Introducing the sprite image in the markup

For this solution, we are using a smaller sprite than the one in the example above. It is now composed of two single stars (“on” and “off”).

We place img elements inside the labels. We assume they will have no value without CSS support, thus we “hide” them by setting specific dimensions via their width and height attributes. Note that using 0 with both attributes would show a broken image in some UAs.

<form ...>
    <fieldset>
        <legend>Rating</legend>
        <label  class="one"
                title="1 out of 5">
            <input  name="LandOf"
                    value="1"
                    checked="checked"
                    type="radio"> 1/5
            <img    src="star-sprite.gif"
                    alt=""
                    height="0"
                    width="0">
        </label>
        <label  class="two"
                title="2 out of 5">
            <input  name="LandOf"
                    value="2"
                    type="radio"> 2/5
            <img    src="star-sprite.gif"
                    alt="" height="0"
                    width="0">
        </label>
        <label  class="three"
                title="3 out of 5">
            <input  name="LandOf"
                    value="3"
                    type="radio"> 3/5
            <img    src="star-sprite.gif"
                    alt=""
                    height="0"
                    width="0">
        </label>
        <label  class="four"
                title="4 out of 5">
            <input  name="LandOf"
                    value="4"
                    type="radio"> 4/5
            <img    src="star-sprite.gif"
                    alt=""
                    height="0"
                    width="0">
        </label>
        <label  class="five"
                title="5 out of 5">
            <input  name="LandOf"
                    value="5"
                    type="radio"> 5/5
            <img    src="star-sprite.gif"
                    alt=""
                    height="0"
                    width="0">
        </label>
    </fieldset>
</form>

Note that with the above markup, we can expect (in most browsers) field selection via label selection.

Considering Accessibility

Unfortunately, as is, this markup creates issues in at least two screen-readers: JAWS and NVDA (see test case for these bugs). The problem is related to the use of a title attribute and an empty string for alternate text.

The workaround to not confuse screen-reader users is to use “stars” as alternate text (alt) and use JavaScript to insert title on mouseover.

Better Markup

<fieldset>
    <legend>Rating</legend>
    <label>
        <img    src="img/small-sprite.gif"
                width="0"
                height="1"
                alt="stars">
        <input  type="radio"
                name="movie"
                value="1_5"> 1/5
    </label><br>
    <label>
        <img    src="img/small-sprite.gif"
                width="0"
                height="1"
                alt="stars">
        <input  type="radio"
                name="movie"
                value="2_5"> 2/5
    </label><br>
    <label>
        <img    src="img/small-sprite.gif"
                width="0"
                height="1"
                alt="stars">
        <input  type="radio"
                name="movie"
                value="3_5"> 3/5
    </label><br>
    <label>
        <img    src="img/small-sprite.gif"
                width="0"
                height="1"
                alt="stars">
        <input  type="radio"
                name="movie"
                value="4_5"> 4/5
    </label><br>
    <label>
        <img    src="img/small-sprite.gif"
                width="0"
                height="1"
                alt="stars">
        <input  type="radio"
                name="movie"
                value="5_5"> 5/5
    </label>
</fieldset>

Result

Rating






Styling

Giving dimensions to the image via CSS

We use em to allow the image to grow or shrink depending on font-size.

Markup

Unchanged

CSS

img {
    width:2.8em;
    height:1.4em;
}

Result

Rating






As you can see already, clicking on an image selects the corresponding radio button. There is no need for scripting as implicit labeling produces this behavior (except in IE).

Removing the image from the flow

Styling the label with position:relative and the image with position:absolute with top/left values is enough to hide input and text inside the labels.

Markup

Unchanged

CSS

label {
    position:relative;
}
img {
    width:2.8em;
    height:1.4em;
    position:absolute;
    top:0;
    left:0;
}

Result

Rating






Displaying one star per label

We style the label so its dimensions match the height and width of a single star.

Markup

Unchanged

CSS

label {
    position:relative;
    height:1.4em;
    width:1.4em;
    overflow:hidden;
    display:block;
}
img {
      width:2.8em;
      height:1.4em;
      position:absolute;
      top:0;
      left:0;
}

Result

Rating






Displaying the stars horizontally

We remove the brs and we float the labels.

Markup

Unchanged

CSS

br {
    display:none;
}
label {
    position:relative;
    height:1.4em;
    width:1.4em;
    overflow:hidden;
    float:left;
}
img {
    width:2.8em;
    height:1.4em;
    position:absolute;
    top:0;
    left:0;
}

Result

Rating






Displaying the sprite image depending on rating

To set a “3 out of 5” rating, we apply the same class to the last two labels. This class will shift the position of the image inside the label.

Markup

<fieldset>
    <legend>Rating</legend>
    <label>
        <img    src="img/small-sprite.gif"
                width="0"
                height="1"
                alt="stars">
        <input  type="radio"
                name="movie"
                value="1_5"> 1/5
    </label><br>
    <label>
        <img    src="img/small-sprite.gif"
                width="0"
                height="1"
                alt="stars">
        <input  type="radio"
                name="movie"
                value="2_5"> 2/5
    </label><br>
    <label>
        <img    src="img/small-sprite.gif"
                width="0"
                height="1"
                alt="stars">
        <input  type="radio"
                name="movie"
                value="3_5"> 3/5
    </label><br>
    <label class="no_star">
        <img    src="img/small-sprite.gif"
                width="0"
                height="1"
                alt="stars">
        <input  type="radio"
                name="movie"
                value="4_5"> 4/5
    </label><br>
    <label class="no_star">
        <img    src="img/small-sprite.gif"
                width="0"
                height="1"
                alt="stars">
        <input  type="radio"
                name="movie"
                value="5_5"> 5/5
    </label>
</fieldset>

CSS

br {
    display:none;
}
label {
    position:relative;
    height:1.4em;
    width:1.4em;
    overflow:hidden;
    float:left;
}
img {
    width:2.8em;
    height:1.4em;
    position:absolute;
    top:0;
    left:0;
}
.no_star img {
    left:-1.4em;
}

Result

Rating






Not relying on image alone to display information

It’s important to offer an alternative to the display of stars in case images are not available. This is because labels and radio buttons are styled to be on top of each other. A simple solution is to move input and text off-screen (i.e. using text-indent:-999em) and apply a background color to the labels.

Markup

No change

CSS

br {
    display:none;
}
label {
    position:relative;
    height:1.4em;
    width:1.4em;
    overflow:hidden;
    float:left;
    background:teal;
    margin-right:1px;
    text-indent:-999em;
}
img {
    width:2.8em;
    height:1.4em;
    position:absolute;
    top:0;
    left:0;
}
.no_star {
    background:#ccc;
}
.no_star img {
    left:-1.4em;
}

Note:

Result

Rating






Finishing touch

Markup

Unchanged

CSS

br {
    display:none;
}
label {
    position:relative;
    height:1.4em;
    width:1.4em;
    overflow:hidden;
    float:left;
    background:teal;
    margin-right:1px;
    text-indent:-999em;
}
input {
    position:absolute;
    left:-999em;
    top:.5em;
}
img {
    width:2.8em;
    height:1.4em;
    position:absolute;
    top:0;
    left:0;
    cursor: pointer;
}
.no_star {
    background:#ccc;
}
.no_star img {
    left:-1.4em;
}
label:hover {
    opacity:.5;
    filter:alpha(opacity=50);
}
fieldset {
    border:0;
}
legend {
    text-indent:-999em;
}

(Note: label:hover is ignored by IE6 and in Opera the background color bleeds through the images. In the demo page, instead of using opacity, I am using a different sprite that shows four states.)

Result

Rating






Displaying the ratings without allowing user interaction

We can make the ratings “read-only” by adding disabled and checked attributes in the appropriate input fields.

Markup

<fieldset>
    <legend>Rating</legend>
    <label>
        <img    src="img/small-sprite.gif"
                width="0"
                height="1"
                alt="stars">
        <input  type="radio"
                name="movie"
                value="1_5"
                disabled> 1/5
    </label><br>
    <label>
        <img    src="img/small-sprite.gif"
                width="0"
                height="1"
                alt="stars">
        <input  type="radio"
                name="movie"
                value="2_5"
                disabled> 2/5
    </label><br>
    <label>
        <img    src="img/small-sprite.gif"
                width="0"
                height="1"
                alt="stars">
        <input  type="radio"
                name="movie"
                value="3_5"
                checked="checked"> 3/5
    </label><br>
    <label class="no_star">
        <img    src="img/small-sprite.gif"
                width="0"
                height="1"
                alt="stars">
        <input  type="radio"
                name="movie"
                value="4_5"
                disabled> 4/5
    </label><br>
    <label class="no_star">
        <img    src="img/small-sprite.gif"
                width="0"
                height="1"
                alt="stars">
        <input  type="radio"
                name="movie"
                value="5_5"
                disabled> 5/5
    </label>
</fieldset>

CSS

The rule using :hover has been removed

Result

Rating






Giving more thought to the process

At this point, it is possible to cast votes without script support, but sighted users have no clue about their selection. So we use JavaScript to:

At the same time, we take advantage of using a script to insert title attributes that will create “tooltips” when users hover over the labels/stars.

Because of the lack of feedback regarding selection without JavaScript, we style labels and form controls only if there is script support. To do so we use JavaScript to set a flag on the html element and then we create a rule based on descendant selectors containing that hook. If the flag is missing, that rule does not apply and elements are not styled.

This is the demo page, the final product. To see how this solution behaves according to various settings, you may want to use your favorite developer tools to increase text-size, break image paths, disable JavaScript, turn CSS off, and more…

Wrap up

Coming up with a “acceptable” solution requires to identify users’ needs, User Agents’ peculiarities, User Agents’ settings and more… Which means extensive testing.

In this process, users’ feedback is essential because following best practices is not always a sure thing. For example, as mentioned earlier, setting no value for the alt attribute of the images within the labels seem to be the safe thing to do, but it turns out that it creates issues with at least two screenreaders (see test case).

Also, feedback from assistive devices’ users allows to ignore some validation error messages - as the one that the Firefox Accessibility Toolbar reports (according to http://bestpractices.cita.uiuc.edu/html/nav/form/).

The goal here was not to fix everything, though. Being able to cast votes without a pointing device was one of my priorities, but improving the look and feel of the solution in Opera when images are disabled is not something I consider essential.

The most interesting part of this “journey” was to make the solution accessible to many users under various conditions, addressing issues such as:

It is also nice to know that this technique relies on img elements rather than background images, which allows the stars to:

All of this comes without sacrificing performance, as this solution relies on this single sprite: stars

Late finding

I recently discovered the system Amazon has built for its voting page. It is quite interesting as they serve a different solution depending on script support. If there is script support, they use an image <map> (interesting approach), if there is no script support they use radio buttons. In both cases, the solution is accessible to keyboard users, and this helps to maximize access to a feature that is a core differentiator for the Amazon platform.

Note that they do not use JavaScript to replace the radio buttons with a image <map>; instead, they use noscript elements in which table markup contains radio buttons.

“Out of the box” solutions

Dreamweaver®
Spry Rating Widget
YUI
Star Rating Script for YUI
Star Rating script with YUI
JQuery
Half-Star Rating Plugin
jQuery Ajax Rater
Simple Star Rating System
5 star rating system in PHP, MySQL and jQuery
WordPress
GD Star Rating System for WordPress
GD Star Rating
Star Rating for Reviews
Flash
5 Star rating system component
Misc.
How a star rating should be
Starry widget 2

Special thanks

Special thanks to Victor Tsaran and Todd Kloots for their valuable feedback.