Using Font Awesome in an Accessible, Bindable Star Rating Control


banner

Creating a rating control that works across browsers, looks good and can be taken advantage of by the MVC Framework requires a little CSS know-how, a couple of freely-available frameworks and some convention-following. There’s a bit of work to get it set up, but it’s reusable, accessible and gosh darn it, it looks good, too.

Don’t overlook the accessible bits! It’s important to remember that most organizations and agencies have policies in place ensuring that folks with disabilities can use alternate methods to view your site. While I am by no means an expert in this area I do have the help of a friend who will try things out for me from time to time in a screen reader and I try to keep these concerns at the front of my mind when doing something “off course”.

Our Starting Point

I’m building off of the work in the last post, where I added Font Awesome to our Bootstrap-enabled MVC 5 web site. Let’s drop a model class for a movie in our project in the Models folder that looks like this:

public class Movie
{
    public int MovieId { get; set; }
    public string Title { get; set; }
    [UIHint("StarRating")]
    public int Rating { get; set; }
}
Next, let’s update the HomeController to have the following actions:
public ActionResult Index()
{
    var movie = new Movie
    {
        Title = "The Last Starfighter", 
        Rating = 4
    };
    return View(movie);
}

[HttpPost]
public ActionResult Index(Movie movie)
{
    return View("Index", movie);
}

Nothing too fancy here, just a GET and POST action for Index requests. The GET populates (simulates) someone navigating to an item that they can rate. The POST allows you to set a breakpoint so that you can inspect the movie result when you submit.

Finally, scaffold a view for Index. First, delete the Index.cshtml from the Views/Home folder in your project. Next, right-click on one of the the Index methods above and select Add View, then choose the Edit template (don’t create a partial view here) and let it build the page for you.  Finally, run your project to see what you get; it should look like this:

image

That’s okay, but, here’s what we want to see:

image

Getting Stylish

So, believe it or not, those are just radio buttons! There’s a couple of things we do to make it come together, including the use of a container DIV, but it’s mostly straightforward. Let’s look at the basic HTML structure that makes this tick.

<div class=”rating”>
<input type=”radio” id=”star5-Rating” name=”Rating” value=”5”>
<label for=”star5-Rating” title=”Best”>5 stars</label>

<!– …more like these… –>

<input type=”radio” id=”star1-Rating” name=”Rating” value=”1”>
<label for=”star1-Rating” title=”Pretty Bad”>1 star</label>
</div>

The container holds a collection of inputs and labels. These elements are attributed as you’d expect, but are inserted in the document in reverse order so that “5 stars” is at the top and “1 star” is at the bottom. This has to do with CSS bits, which we’ll come to in a minute.

Oh…here we are, the CSS bits!

Right, so we start by setting up the container and getting our input off-screen.

.rating {
float:left;
}

.rating:not(:checked) > input {
position:absolute;
top:-9999px;
clip:rect(0,0,0,0);
}

This works okay because if a user taps, clicks or selects a label with a screen reader, the input is still appropriately checked. Next, we wire up the label.

.rating:not(:checked) > label {
float:right;
width:1em;
padding:0 .1em;
overflow:hidden;
white-space:nowrap;
cursor:pointer;
font-size:3em;
line-height:1.2;
color:#e0e0e0;
}

.rating:not(:checked) > label:before {
font-family: FontAwesome;
content: “\f005”;
}

These two styles push the label off-screen and setup the fixings for our star, including the insert of the character from Font Awesome.

We float these to the right so that we correct our order; remember that we inserted the stars in reverse order in our HTML, here we use CSS to correct that. This workaround is needed for the foreseeable future as we do not have CSS selectors to help us resolve things “before” our element, only after. By putting rating 1 after rating 3, we can select 3, 2 and 1 with various combinations of selectors; however, there is no mechanism allowing us to select the other way, i.e., “before”.  (The before psuedo-selector allows us to insert content, but not apply style transformations.)

Finally, we handle the color states with various combinations to handle selected stars, stars that were previously selected and stars that are hovered (but not yet selected).

.rating > input:checked ~ label {
color: #fa0;
}

.rating:not(:checked) > label:hover,
.rating:not(:checked) > label:hover ~ label {
color: #fe0;
}

.rating > input:checked + label:hover,
.rating > input:checked + label:hover ~ label,
.rating > input:checked ~ label:hover,
.rating > input:checked ~ label:hover ~ label,
.rating > label:hover ~ input:checked ~ label {
color: #ea0;
}

It may seem like there’s a lot of action going on in that code, but it’s required. The >, +, ~ and : selectors in there allow us to be very specific about which elements in which state require coloring.

Add all of those CSS bits to your Site.css file (in the Content folder) and your styles will be in place. All that’s left to do now is to take that rough-in of HTML we did before and turn it into something the MVC Framework can use.

Bringing Some MVC to the Equation

You’ll have noticed the attribute UIHint decorating our Rating property on the Movie model class that we created in the first step. That was no accident! Now, let’s get that working for us by creating a reusable partial view in our project. Add a file called StarRating.cshtml to the Views\Shared\EditorTemplates folder. By convention, this is the folder that the MVC Framework checks for custom views used to render properties or models (you can see another sample here from my Bootstrap series).

The StarRating.cshtml should look like this:

@model int

@{
var chk = “checked”;
var checked1 = Model == 1 ? chk : null;
var checked2 = Model == 2 ? chk : null;
var checked3 = Model == 3 ? chk : null;
var checked4 = Model == 4 ? chk : null;
var checked5 = Model == 5 ? chk : null;
var htmlField = ViewData.TemplateInfo.HtmlFieldPrefix;
}

<div class=”rating”>
<input type=”radio” checked=”@checked5” id=”star5-@htmlField” name=”@htmlField” value=”5” /><label for=”star5-@htmlField” title=”Best”>5 stars</label>
<input type=”radio” checked=”@checked4” id=”star4-@htmlField” name=”@htmlField” value=”4” /><label for=”star4-@htmlField” title=”Good”>4 stars</label>
<input type=”radio” checked=”@checked3” id=”star3-@htmlField” name=”@htmlField” value=”3” /><label for=”star3-@htmlField” title=”Average”>3 stars</label>
<input type=”radio” checked=”@checked2” id=”star2-@htmlField” name=”@htmlField” value=”2” /><label for=”star2-@htmlField” title=”Not Great”>2 stars</label>
<input type=”radio” checked=”@checked1” id=”star1-@htmlField” name=”@htmlField” value=”1” /><label for=”star1-@htmlField” title=”Pretty Bad”>1 star</label>
</div>

Here’s a breakdown of the interesting bits that make this work:

  • The model is of type int, allowing it to be rendered for properties of the same type.
  • There is the use of nullable attributes in play, set up with the check against the model so that we can correctly render which star is to be selected. This is important when our model already has a value.
  • The radio buttons are ordered in reverse and take their name from the ViewData.TemplateInfo.HtmlFieldPrefix value.* We use an ID on each star, and a label that is related back to that input. This allows selection via the label (you should always use this!)

Viola! With the CSS from above in place, the custom editor template saved and the UIHint on your Movie model class, your page will now be rendered with a star rating control for your movie.

Set a breakpoint in your POST action of your controller. If you submit your form while debugging, you will see that the value you have selected is indeed bound to the property of the Movie class. Huzzah!

Credits

I have leaned on many star rating controls over the last decade, but have come back to variants of this one (based on this work from @Lea Verou) for the last two or three years now. I would like to stress that I haven’t yet found a perfect control (this one, for example works well with up/down arrows, but not left/right) and I am sure this version won’t work for all people in all scenarios. Also, the idea for this example came from the Font Awesome site, but the astute reader would soon realize why the basic example on their site would not be a good candidate for MVC (primarily because of the lack of structure to accommodate multiple controls on the same page).

Next Steps

If you’ve built the sample along with the article, you’ll likely have had at least a few ideas on how you can create reusable controls for your project. Be sure to check out my Bootstrap series or purchase a copy of my book for more examples like this, and happy coding!