A List Apart

Menu
Issue № 395

Accessibility: The Missing Ingredient

by Published in HTML, Accessibility · 18 Comments

Once upon a time, I treated web accessibility as something of an extra. Sure, my images had alt attributes. Yes, my anchors contained titles. I honored 508 compliance, too, but it was usually the last thing I did. The way I saw it, the cake was ready to eat; accessibility was the decorative icing to slather on at the end.

Article Continues Below

Sadly, I wasn’t alone. Many developers I’ve recently spoken with seem to place accessibility last, if they address it at all. Why is accessibility often treated as an afterthought? Key factors include lack of tools and specifications, weak industry demand, and developer laziness.

Back in the late ’90s, web accessibility was primitive at best. JAWS was still in its infancy, and WAI-ARIA had yet to be conceived. All that seemed to matter was how quickly the user could be visually impressed. DHTML and Flash wizards ruled the land. Flannels looked best worn around the waist.

As accessibility began to gather steam in 2004–2005, many developers and companies still paid little attention to the subject. Web developers were lost in CSS black magic, PHP tutorials, and JavaScript books. As a journeyman freelancer, I had never heard accessibility mentioned as a project requirement.

Despite the now well-publicized and supported WAI-ARIA, a specification engineered specifically to help impaired users, we developers often do not implement it. Perhaps we’re waiting for an accessibility-minded client to catalyze the learning process. Perhaps we feel overwhelmed at where to start. Whatever the excuse, it’s no longer valid given the resources and technology currently available.

A few months into my employment at IBM, the web application my colleagues and I had been devoting our weekly hours to had to satisfy a stringent accessibility checklist. We needed to thoroughly test the entire web application against WAI-ARIA and WCAG compliance. I was suddenly tasked with creating a comprehensive plan detailing the open issues, as well as estimating how long it would take for the team to address them. Placing accessibility last for so many years left me thinly educated and unprepared.

Through trial and error, the help of my knowledgeable colleagues, and much reading on the subject, I’ve emerged as a changed developer. Coding accessibly is not an extra thing to consider at the end of a project, but simply another thing to consider from the beginning. Let’s walk through a homegrown example that showcases some useful WAI-ARIA roles, states, and properties.

Before we begin

Screen readers, like browsers, vary among vendors. My intention is not to provide an exhaustive tutorial on this broad topic. I chose ChromeVox for this tutorial because it’s a free screen reader plugin that runs on both Mac and Windows. However, many other screen readers exist. For web development involving WAI-ARIA, here are some popular choices.

Windows users

Mac users

Linux users

As mentioned above, each reader is unique. If you’re following this demo with a reader other than ChromeVox, you’ll notice differences in how the assistive technology parses ARIA.

What are we trying to accomplish?

Our client would like a shopping cart for his robot parts business, Robot Shopper. The cart must meet WAI-ARIA guidelines and work smoothly from a keyboard. To test our cart on a screen reader, we’re going to use ChromeVox, which will allow us to aurally experience our web page. After we’re through, you may wish to disable the plugin. To do that, visit chrome://extensions and untick the box beside the ChromeVox plugin.

To manage items in the cart, we’ll use HTML5 storage, and we’ll employ some JavaScript templating using Handlebars to display our inventory markup. As for the design, here’s what we’re aiming for:

Initial display with the cart closed.
Initial display with the cart closed.
The cart is opened and empty.
The cart is opened and empty.
The cart is opened with items.
The cart is opened with items.

Beginning markup

first_pass/index.html

<body role="application">

	<div id="container">
		<header>
			<h1>Robot Shopper</h1>

			<button
				title="Cart count"
				id="shopping_cart">
			</button>
		</header>

		<!-- dynamically loaded from our template -->

		<div
			id="main"
			tabindex="-1">
		</div>

		<footer>
			<p>Robot Shopper © 2014</p>
		</footer>
	</div>

	<div
		id="my_cart"
		tabindex="0">

		<div id="my_cart_contents"></div>

		<button
			title="Close dialog"
			id="close_cart">
			x
		</button>
	</div>

	<div id="page_mask"></div>
	
</body>

The application role

Although I didn’t plan on diving into ARIA roles in our first pass of the code, adding the application role to the body is mandatory for the experience we wish to create. This small passage from the Yahoo! Accessibility blog really captures the gist of this role:

Your average web page is a document. The user expects to read content and it may feature some interactive behaviors. Applications are more like a desktop application. The user expects tool sets, instant changes, dynamic interactions.

One must-have feature of our Robot Shopper application is for users to navigate quickly between product sections using the up and down arrow keys. For certain screen readers, such as NVDA, removing the application role will prevent our custom JavaScript from overriding these keyboard events. Without the application role, pressing the up and down arrows will instead move the focus between every element on the page—not what we want. Using this role, we’re informing the browser, “I know what I’m doing. Don’t override my defined behaviors.” Keep in mind that any region we declare as an application requires us, the developers, to implement custom keyboard navigation.

Our template

You may have noticed above that our main div designated for storing all of our products is empty. That’s because our Handlebars template immediately loads our JSON product data into this element.

first_pass/index.html

<script id="all_products" type="text/x-handlebars-template">
	<div id="product_sections">
		{{#products}}
			<section tabindex="-1">
				<div class="product_information">
					<h2 class="product_title">{{title}}</h2>
					<span class="product_price">${{price}}</span>
				</div>
				<div class="product_details">
					<a href="#" title="{{title}} details">
						<img class="product_thumb" src="{{img_uri}}" alt="{{title}}">
					</a>
					<p class="product_description">{{description}}</p>
				</div>
				<button
					title="Add to Cart"
					class="button add_to_cart"
					tab-index="0"
					data-pid="{{pid}}">
					Add to Cart
				</button>
			</section>
		{{/products}}
	</div>
</script>

first_pass/products.js

data = { products: [
	{
		title: "Jet Engines",
		price: "500.00",
		img_uri:	"http://goo.gl/riaO3q",
		description: "It's only a jetpack--a simple, high-thrust, fuel-thirsty jetpack. I would add this puppy to the cart. What could go wrong?",
		pid: "product_093A14"
	},
	
	…

]};
document.getElementById("main").innerHTML = template(data);

ChromeVox time (Demo)

Let’s check out our Robot Shopper in action. In the demo, click “Add to Cart” and see the cart count in the blue robot’s chest increase. Simple, right? But how does this experience translate within the context of a screen reader?

First, let’s talk page navigation. Although we can tab to product detail anchors and “Add to Cart” buttons, it takes users two tab presses to traverse each item—a chore if a disabled user wishes to reach an item far down on the list using a keyboard. We’ve solved this issue by adding keyboard arrow navigation.

first_pass/app.js

$(document.documentElement).keyup(function (event) {

	…

	if (key_pressed === UP) {
		direction = "prev";
	} else if (key_pressed === DOWN) {
		direction = "next";
	}

	…

$(".selected")[direction]()
	.addClass("selected")
	.focus()
	.siblings()
	.removeClass("selected");
	
	…
		
});

One interesting thing about this code is that we’ve associated the arrow key pressed with the direction we’d like to move in the product list. The directional string ("prev" and "next") becomes an interpolated jQuery function that does the moving for us. Using the up and down arrow keys, we can move between items more quickly and efficiently. After arrowing (or clicking) to the product section we’re interested in, we can then tab to each product’s anchored thumbnail or “Add to Cart” button.

A lackluster cart experience

From an empty cart, we click an “Add to Cart” button. Sighted users can see the number on the blue robot’s chest increment. Non-sighted users only hear, “Add to Cart button.”

Clicking the blue robot launches the cart modal. Of course, a user wouldn’t know that unless she could see it happening. Users dependent on screen readers will hear, “One button,” or the number of items in the cart.

Once we’re in the visible modal, we can increase, decrease, and remove all items from the cart. In our first pass, however, accessing those buttons leads to a rather poor UX for visually disabled users. “Plus button” isn’t a good enough response from our screen reader when we hit the “+” button.

Similarly, when closing the cart modal using our “x” button, our reader tells us, “x button.”

It’s not the fault of the screen reader, either. Our reader doesn’t have what it needs to offer a rich experience to users dependent on it. To provide a rich experience for non-sighted users, we must improve our semantics using WAI-ARIA roles, states and properties.

First pass fixes

We’ll approach our accessibility enhancements using a general-to-specific approach.

1. Landmark roles

Although somewhat semantic, our first pass markup contains no landmark roles. Why use landmark roles? According to Steve Faulkner, accessibility guru and advocate:

Adding ARIA landmarks to your existing site, or to a site you are developing, provides useful global navigation features and aids understanding of content structure for users.

Landmark roles allow users of a screen reader to navigate to major sections of your website with a single keystroke. Our markup also lacks ARIA states and properties. These states and properties are terms, which, according to the W3C, “provide specific information about an object, and both form part of the definition of the nature of roles.”

second_pass/index.html

<!-- Header -->
<header role="banner">

<!-- Main content element replaces original div element-->
<main
	id="main"
	tabindex="-1">
</main>

If we’d chosen to remain with our div in parenting our main content, adding role="main" to it would be acceptable for accessibility purposes. However, we’re going to forgo the role in lieu of the straightforward main tag.

Cart dialog:

second_pass/index.html
<div
	id="my_cart"
	tabindex="0"
	role="dialog"
	aria-hidden="true"
	aria-labelledby="my_cart_title"></div>

When items are first added to the cart, markup is dynamically created using code in the updateCartDialog function. Sighted users will notice that the cart is not yet visible. For non-sighted users, we need to ensure the cart is not announced by our screen reader. To do this, we’ve added the aria-hidden attribute which, according to the specification, sets a state on a given element “indicating that the element and all of its descendants are not visible or perceivable to any user as implemented by the author.” It goes on to say, “if an element is only visible after some user action, authors MUST set the aria-hidden attribute to true.” The cart dialog is labeled by this table’s caption element.

second_pass/app.js
$cart_contents
	.html("")
	.append("\
	<table id='cart_summary'>\
		<caption id='my_cart_title'>Cart Contents</caption>\
	…
	
	</table>\

2. Adding/removing items from the cart should notify the user

Clicking the “Add to Cart” button triggers ChromeVox to say, “Add to Cart button,” which isn’t very helpful to a user dependent on aural cues. Users should know what exactly is being added to the cart. Also, the action of adding and removing items from within the cart modal should audibly inform the user how many items are in the cart the moment the cart count changes.

To make this happen, we first modify the “Add to Cart” button markup in our Handlebars template.

second_pass/index.html
<script id="all_products" type="text/x-handlebars-template">
	
	…
	
	<button
		class="button add_to_cart"
		tabindex="0"
		aria-label="Add {{title}} to the cart"
		aria-controls="shopping_cart cart_count"
		data-pid="{{pid}}">
		Add to Cart
	</button>
	
	…

</script>

The action associated with this button controls the shopping_cart and cart_count elements by incrementing the total number of items in the cart.

<script id="all_products" type="text/x-handlebars-template">
	
	…
	
	<button
		title="Cart Count"
		id="shopping_cart"
		aria-owns="cart_contents"
		aria-label="Cart count">
	</button>
	
	…
	
</script>

The shopping cart button (the blue robot) stores the current number of items in the cart. However, to audibly notify the user of changes as they occur, we’ll take a new approach.

<div
	class="aria_counter"
	id="cart_count"
	aria-live="polite">
</div>

As the user adds an item to the cart, we update our aria counter which contains an aria-live attribute. An element using this attribute will announce its contents when something changes.

3. Cart dialog buttons need ARIA properties

Cart dialog buttons.
Cart dialog buttons.

3a. “x” button (remove all)

As stated earlier, clicking “x” currently prompts ChromeVox to say, “x button.”

first_pass/app.js
<button class='button row_button remove_row_items' title='Remove all items of this type' data-pid='" + element.pid + "'>x</button>

Our improved “x” button related code lives in the updateCartDialog function.

second_pass/app.js
<button aria-controls='row_" + index  +  " cart_count item_count' class='button row_button remove_row_items' aria-label='Remove all " + element.title + "s from the cart?' data-pid='" + element.pid + "'>x</button>\

Introducing the aria-controls attribute to our button provides more semantic meaning. We’re informing screen readers that a given row in our cart is controlled by the “x” button within each row, and that we can remove the entire row by clicking the button and confirming the removal. In addition, the “x” button is now labeled, so screen reader users will be told which exact item is to be removed from the cart.

3b. “−” button (decrease quantity of specific item)

When clicked, ChromeVox reads aloud, “dash button.” We store the id of the target quantity field the button changes within a non-semantic data attribute.

first_pass/app.js
<button class='button row_button decrement_row_item'  title='Decrease quantity by one' data-pid='" + element.pid + "' data-product-quantity='product_quantity_" + index  + "'>-</button>

…

$(document).on("click", ".decrement_row_item", function() {
	app.decrementItemQuantity(this.dataset.pid, this.dataset.productQuantity);
	
	…

});

Let’s do away with the non-semantic data attribute in place of a semantic ARIA attribute. Here’s our improved version:

second_pass/app.js
<button aria-controls='product_quantity_" + index  + " cart_count item_count' class='button row_button decrement_row_item' aria-label='Decrease " + item_title + " quantity' data-pid='" + element.pid + "'>-</button>

$(document).on("click", ".decrement_row_item", function() {
	var aria_controls = $(this).attr("aria-controls").split(" ")[0];
	app.decrementItemQuantity(this.dataset.pid, aria_controls);
	
	…
  
});

The “-” button is now labeled. For a better UX, our label now includes the product title. Adding aria-controls to the mix, we declare the elements in the DOM that are controlled by this button:

  1. Product quantity: the td containing the current product quantity
  2. Cart count: the off-screen cart counter whose value is announced when the number of items in the cart changes
  3. Item count: another off-screen counter whose value is announced when the quantity of each item in the cart changes

Here is the JS handling the button click:

$(document).on("click", ".decrement_row_item", function() {
	// get the product quantity id of the row
	var aria_controls = $(this).attr("aria-controls").split(" ")[0];
	app.decrementItemQuantity(this.dataset.pid, aria_controls);
	
	…
	
});

Improving our markup has the added benefit of making our JS more specific, too. Our second argument to decrementItemQuantity becomes part of the value of the aria-controls attribute. So, not only is the button more accessible, our code has become more readable.

4. Cart modal needs ARIA state

Currently, when we launch the cart modal, ChromeVox doesn’t indicate this detail to the user. This is because we only add and remove a semantically weak class name on the body to get this interaction rolling.

first_pass/app.js
showModal: function() {
	$("body").addClass("show_cart");
},

removeModal: function() {
	$("body").removeClass("show_cart");
}

To make our reader perform better, we’ll feed it better semantics. When the cart is open, we’d like to conceal our container (everything beneath the cart modal) from assistive technologies. Conversely, our cart now comes out of hiding and is clearly revealed to assistive technologies.

second_pass/app.js
showModal: function() {
	if (app.elements.$my_cart.attr("aria-hidden") === "true") {
		$("body").addClass("show_cart");
		
		…
		
		app.elements.$container.attr("aria-hidden", "true");
		app.elements.$my_cart.attr("aria-hidden", "false");
	}
	
	…
	
}

5. Selected product sections need ARIA state

When users press the up and down arrow keys, a “selected” class is added to represent the currently selected product section. Let’s trade that class for aria-selected.

second_pass/app.js
$(document.documentElement).keyup(function(event) {

	…

	if (key_pressed === UP) {
		direction = "prev";
	} else if (key_pressed === DOWN) {
		direction = "next";
	}
	
	…
	
	$selected[direction]()
		.attr("aria-selected", "true")
		.focus()
		.siblings()
		.attr("aria-selected", "false");
		
	…
		
});

Round 2 (Demo)

Follow along and notice how we’ve improved the UX for disabled users through better semantics.

  1. From an empty cart, add any item to the cart.
    • Before: “Add to cart button”
    • After: “Add {{title of product}} to the cart button. Cart count one”
  2. Tab to focus on the robot.

    • Before: “One button”
    • After: “Banner, cart count one”
  3. Click the robot to open the cart.

    • Before: (contents of the cart)
    • After: “Enter dialog” (contents of the cart)
  4. Click the “+” button in the opened cart containing one item.

    • Before: “Plus button”
    • After: “Increase {{title of product }} quantity button. Item quantity two. Cart count two”
  5. Click the “x” to remove all items of a certain type.

    • Before: “X button”
    • After: “Remove all {{title of product}}s from the cart button”
  6. Cancel out of the JavaScript confirmation pop-up and exit the modal dialog using the escape key or “x” button.

    • Before: “”
    • After: “Exited dialog”

Our focus then returns to the last selected product before the cart was opened. If no product section had been previously selected, focus returns to the first product section in the list.

Wrapping up

I’d be a liar if I told you writing accessible code takes no extra work. When prototyping and sketching with our product designer, we scrutinize our decisions even more heavily than before. Each design must lead to an accessible UX. Similarly, developers unfamiliar with accessibility practices may spend more time revising their commits before seeing their code merged.

Good product managers realize that new challenges beget greater time requirements from designers and developers. Encouragingly, we are highly adaptable creatures; these up-front labor costs will diminish over time. Most important, this hard work will yield a more usable product for all customers and a more readable codebase for developers. Let’s agree not to treat accessibility as the icing on the cake, but rather as an essential part of the mix.

18 Reader Comments

Load Comments