A List Apart


Illustration by Kevin Cornell

ARIA and Progressive Enhancement

For seven years, we’ve held tightly to the belief that using progressive enhancement is the right way to build websites and applications. Progressive enhancement is how we build sustainable, interoperable, and accessible web solutions.

Article Continues Below

You’ve seen this before. You…

  1. start with pristine, semantic HTML,
  2. provide a layer of presentational suggestions via CSS (these may or may not be overridden by user styles), and
  3. provide a layer of behavioral enhancements with JavaScript (again, these may or may not be overridden or supplemented by user scripts).

At each step along the way, we have what amounts to a working solution. We build each working solution on top of other, already working solutions.

Now that the release of ARIA is approaching, let’s take a look at how ARIA fits within progressive enhancement strategy. Can we use ARIA in a way that respects progressive enhancement? Can we use ARIA in ways that ensure we have a working solution at every level?

ARIA simplified

ARIA is an emerging specification from the W3C and is an accessibility effort that a number of companies support. ARIA is designed to provide accessibility at a technical level—what you might call “programmatic accessibility”—where it doesn’t already exist. For example, many of the date pickers and other advanced widgets that we add to our websites are nothing more than a collection of <div> and <span> elements that have no semantic meaning; ARIA attempts to provide that semantic meaning.

Keep in mind that ARIA is still not quite finished—it is, as of this writing, published as a working draft and is under review. This is how specs work. In fact, if you read Martin Kliehm’s 2007 article from ALA 235, Accessible Web 2.0 Applications with WAI-ARIA, you’ll see that quite a bit has changed. The general ARIA concepts are still the same, but the current implementation is much simpler and will be much easier to use.

Eventually, implementing ARIA in our designs will allow us to reliably tell assistive technology “this div is actually a dialog box” or “this ordered list of links is actually a menu of choices” or “this collection of divs, images, and spans is a progress bar, and that progress bar is currently at 75%.”

ARIA offers three means to do this. They allow us to communicate:

  • what role a particular component has in the interface,
  • the properties that component has, and
  • the current state of the component.

We do this by using ARIA attributes and values just like we use any other HTML attribute and value, but we modify them with scripting in response to events that happen within the browser. Some of what ARIA provides also helps to establish programmatic relationships between parts of the interface, but we’ll leave that for later.

ARIA in the wild

We recently relaunched Simply Accessible, a home for all of our accessibility-related writing. In the site code we used ARIA landmark roles to provide a standardized set of navigational landmarks within the page. If you look at a sample article page such as Custom Stylesheets for iOS, the article pages have the following landmark roles defined in their code:

  • role=“banner” for the masthead of the site,
  • role=“navigation” for each of the navigation lists at the top and bottom of the page,
  • role=“search” for the search form,
  • role=“main” for the main content of the page,
  • role=“complementary” for the related information in the sidebar, and
  • role=“contentinfo” for the copyright statement at the bottom.

This code allows assistive technology that understands ARIA landmarks to move from one landmark to another using keyboard shortcuts (as with the JAWS screen reader) or gestures (as with Apple’s VoiceOver).

Here’s another example of ARIA in action: A slider widget on a Google Map. In this implementation of a Google Map, we’ve added ARIA to the slider control that allows you to change zoom levels. We added ARIA to this quite simply because there is no native HTML element for a slider—we have to construct it from the tools we have. So, to approximate a real slider that you might see at the operating system level, we added ARIA to the basic HTML markup. Here we have:

  1. indicated a role for the component (role=“slider”),
  2. used aria-orientation=“vertical” to specify that it is…wait for it…oriented vertically, and,
  3. added several properties to that slider (aria-valuemin=“0”, aria-valuemax=“17”, aria-valuetext=“14”, and aria-valuenow=“14”).

As we slide the thumb along the rail to change the zoom level, we use JavaScript to change the value of aria-valuenow and aria-valuetext to match the zoom level.

Showing the aria-valuetext and aria-valuenow with the value of 12

Fig 1. Showing the aria-valuetext and aria-valuenow with the value of 12.

You can see this for yourself using Firebug or another developer tool. Figure 1 shows the aria-valuetext and aria-valuenow highlighted with a value of 12, matching the zoom level from the maps themselves. With JavaScript on, those values update dynamically as you zoom in or out. For true progressive enhancement, you would create a version that uses a form submission to select a zoom level and uses the Google Maps Static API, which doesn’t require JavaScript.

In typical ARIA implementations such as these:

  1. simple ARIA role attributes define the role for various parts of the page: role=“menu”, role=“dialog”, role=“navigation”, and role=“tablist”,
  2. simple ARIA properties define connections between various interface components: aria-labelledby=“someid”, aria-describedby=“anotherid”, aria-haspopup=“true”, and
  3. simple ARIA states define the current state of objects on the page: aria-hidden=“true”, aria-invalid=“false”, aria-disabled=“true”, aria-expanded=“false”.

Go straight to the source for full details on all the ARIA roles and states and properties.

To use ARIA, we’ll need several different pieces of the puzzle to fall into place. As you saw in the slider example, we need to add ARIA attributes to our HTML and then control them with JavaScript as we dynamically update the page.

Support for ARIA isn’t quite that simple, though. Yes, we can add ARIA to provide “semantic sugar,” but there is more to it than that.

Accessibility requires everyone—authors and content creators, browser vendors, assistive technology vendors, and the person using the site or app—to come to the table with the right tools.

What are the right tools for ARIA?

Some browsers have some support for ARIA. Current versions of Firefox 3, Opera 9.5+, IE8/9, and WebKit-based browsers like Safari 4+ all support some aspects of ARIA. Of course, their support isn’t complete. It can’t be, simply because the spec isn’t finished.

Some assistive technology has some support for ARIA. Current versions of JAWS, Window-Eyes, NVDA, VoiceOver, and ZoomText have some support for ARIA, but older versions, naturally, don’t.

Rubber, meet road

A really simple way to start implementing ARIA is by using ARIA’s landmark role concept.

You’ve already seen this if you looked at our Simply Accessible site. But there are a few other things to note in that page relative to progressive enhancement.

Let’s just take a look at one example—the role of “navigation.”

Here’s how we’ve coded the navigation in the <footer> and the <header> sections of the site.

<nav role="navigation">
  <li><a href="/">Home</a></li>
  <li><a href="/archives/">Archives</a></li>
  <li><a href="/about/">About</a></li>
  <li><a href="/contact/">Contact</a></li>

You might be asking “Why on earth is there a role of navigation on an element called <nav>?” And if you weren’t, you should be. These questions are ones that we’ll face every day as we move forward with ARIA.

Here’s the thing: ARIA only has some support in browsers and assistive technology. By the same token, we’ve used the HTML5 <nav> element, which, as you might expect, has only some support in browsers and assistive technology. We can use an element called <nav> and browsers will deal with it reasonably well, but they don’t do anything useful with it, like pass it to the operating system’s accessibility API. Without meaningful support, <nav> might as well just be a <div>.

This means that while <nav> can be styled and will render in a browser, right now, there is nothing that signals a screen reader or any other piece of assistive technology that this page element is being used for navigation. However, some assistive technology recognizes role=“navigation” as a landmark role. So, we double that up, and add a role=“navigation” to the <nav> element.

What about scenarios where neither HTML5’s <nav> nor ARIA’s role=“navigation” is supported? Inside the <nav> we use our trusty unordered list with list items for navigation—the lists we’ve been using for navigation for years.

Now let’s take a look at a slightly more complicated example—specifying required form fields.

Going a bit deeper

One advantage of ARIA markup is that we’ll be able to readily communicate required fields to assistive technology at a programmatic level:

<label for="firstname">First Name</label>
<input type="text" id="firstname" aria-required="true" />

Ultimately this means that a screen reader user may hear “First Name, type in text, required.”

We could also use some CSS to provide visual cues based on this attribute:

input[aria-required=true]:after {

We know that we have some support for ARIA in assistive technologies, but how would this example render right now? Generated content isn’t read by current versions of JAWS and WindowEyes, but can be read by Apple’s VoiceOver. Still, we won’t be able to rely on that, and this generated content relies on an attribute selector that may or may not be supported in your browser. And what of screen readers that don’t support ARIA?

The problem is if you rely on ARIA to communicate the required attribute to screen readers, then that status will not be announced to other assistive technologies that don’t understand ARIA.

To solve this problem, we need to provide another mechanism to communicate the required status, without using ARIA.

“But wait!” you say. “We can use the HTML5 required attribute!”

Yes, you could use the HTML5 required attribute, but again, that will only work where HTML5 is supported and exposed to the accessibility API. In that case, we’ll need yet another method to provide notification of required fields.

So, let’s look at providing another, more traditional method to do this—based on work we did early in 2005 (see Required Form Fields, where we ensure that the required status is coded as part of the label).

When we do this, we provide this information to those assistive technologies in a non-ARIA state. But what is the impact on the assistive technologies that do understand ARIA? We need to take a look at several examples to see what our options are for indicating required fields.

Here’s a walkthrough of some of our basic tests. Let’s go through the simplest scenario first—HTML5’s required attribute.

Here’s an example using the HTML5 required attribute to denote the required field:

<label for="username">User Name</label>
<input type="text" id="username" required />

We all have great hope for this, but, as of this writing, this attribute isn’t exposed to any of the assistive technology we used for testing. The required status was not announced in any way using JAWS 11, Window-Eyes 7.2, NVDA, VoiceOver, or ZoomText. Support for the HTML5 required attribute just isn’t there yet, so we need to find another way.

Another simple method would be to use ARIA alone to denote the required field:

<label for="username">User Name</label>
<input type="text" id="username" aria-required="true" />

In this case, JAWS 11, Window-Eyes 7, NVDA, and ZoomText all supported and announced that the field was required. VoiceOver on the Mac did not announce that the field was required due to a bug that will hopefully be fixed in an update to VoiceOver. That’s reasonable support for this technique to be considered successful at this stage.

Let’s look at the non-ARIA scenario so that we get a complete picture.

Here’s an example using both ARIA and our traditional “in-the-label” method for those scenarios where ARIA isn’t supported:

<label for="username">User Name <em>(required)</em></label>
<input type="text" id="username" aria-required="true" />

In this case, JAWS 11, Window-Eyes 7, NVDA, and ZoomText all announced that the field was required twice. That certainly is better than the scenario where it doesn’t get announced at all, but we’d really like to reduce repetition to make it as easy to understand as possible. VoiceOver doesn’t recognize the text “required” in the label as it is nested inside the <em> element (a result of the same bug mentioned above), and therefore the current version of VoiceOver doesn’t read out the text “required” when in forms mode.

Let’s try to reduce that repetition by our traditional “in-the-label” method and role=“presentation” so that when ARIA is supported, the <em> content isn’t read out:

<label for="username">User Name
   <em role="presentation">(required)%lt;/em>
</label><input type="text" id="username" aria-required="true" />

In this case, JAWS 11, Window-Eyes 7, NVDA, and ZoomText all announced that the field was required twice.


Yes, really. The problem here is the way that role=“presentation” works. It is designed such that when role=“presentation” is added to a node, it tells assistive technology to ignore the semantics of that node, but that doesn’t apply to its child nodes, including text nodes. Bleh. This seems counter-intuitive to me, but it’s a reality that we need to deal with. Surprisingly, VoiceOver actually works in this case—reading out that the field is required, presumably because the role=“presentation” on the <em> element tells it to ignore the semantics. This basically exposes the text content, such that “required” is now read out in VoiceOver.

We even tried using ARIA, our traditional “in-the-label” method and aria-hidden=“true”, so that when ARIA is supported, the <em> content isn’t read out to avoid duplication:

<label for="username">User Name
   <em aria-hidden="true">(required)</em>
</label><input type="text" id="username" aria-required="true" />

This isn’t really what aria-hidden was designed for; it’s intended as a way to express that something is currently hidden. For example, if you have a panel or some other content that you show and hide, you should use aria-hidden=“true” when it is hidden, and aria-hidden=“false” when it is showing. However, the draft spec says this of aria-hidden:

Authors MAY, with caution, use aria-hidden to hide visibly rendered content from assistive technologies only if the act of hiding this content is intended to improve the experience for users of assistive technologies by reducing redundancy. Authors using aria-hidden to hide visible content from screen readers MUST ensure that identical or equivalent meaning and functionality is exposed to assistive technologies.

This sounds perfect for this application doesn’t it? We are expressing equivalent meaning in all scenarios—ARIA and non-ARIA. The support for aria-hidden in our tests was lacking. In this case, JAWS 11, Window-Eyes 7, NVDA, and ZoomText all announced that the field was required twice. VoiceOver does not announce required at all when found in the <em> content and does not appropriately deal with aria-hidden=“true” on this child element of the label, though it seems to work with aria-hidden=“true” on other content. This technique may be useful in the future as support for aria-hidden in assistive technology improves.

This leaves us with one final option (at least for now)—one that I’m not entirely fond of, but here it is:

Here we’re using ARIA, the traditional “in-the-label” method, and role=“presentation” on an image with alt text of “Required”:

<label for="username">User Name
   <img role="presentation" src="required.png" alt="Required" />
</label><input type="text" id="username" aria-required="true" />

The role=“presentation” on the image tells ARIA-supporting browsers and assistive technology to ignore the semantics of that node. That node has alt text of required and in the case where ARIA is supported, it is ignored.

In this final scenario, JAWS 11, Window-Eyes 7, NVDA, and ZoomText all announce that the field is required, precisely once. And in older versions, where ARIA isn’t supported or with browsers that may not communicate ARIA roles and attributes to the accessibility API? They’ll get the non-ARIA version. In this case, VoiceOver does not indicate that the field is required, in all likelihood because of the same bug we’ve talked about in other cases.

Yes, this makes me want to cry, too. If that wasn’t too painful, here’s a summary of these tests in tabular form:

ARIA Tests Results
Zoom Text 9.1JAWS 11Window-Eyes 7.2NVDAVoiceOver
HTML5 requiredDoesn’t announce the field is requiredDoesn’t announce the field is requiredDoesn’t announce the field is requiredDoesn’t announce the field is requiredDoesn’t announce the field is required
ARIA required to match other references to ARIAAnnounces “required” onceAnnounces “required” onceAnnounces “required” onceAnnounces “required” onceAnnounces “required” once
required in label and ARIA requiredAnnounces “required” twiceAnnounces “required” twiceAnnounces “required” twiceAnnounces “required” twiceAnnounces “required” twice
required in label and ARIA required, include role=“presentation”Announces “required” twiceAnnounces “required” twiceAnnounces “required” twiceAnnounces “required” twiceAnnounces “required” once
required in label and aria-hidden=“true”Announces “required” twiceAnnounces “required” twiceAnnounces “required” twiceAnnounces “required” twiceDoesn’t announce the field is required
required in label and ARIA required, include role=“presentation” on an imageAnnounces “required” onceAnnounces “required” onceAnnounces “required” onceAnnounces “required” onceDoesn’t announce the field is required

The problem, as I see it

Part of the problem at present is that we don’t know when ARIA is supported. We need to detect objects and capabilities before we use them; if they are not present or unsupported, we can ensure that we still have a reliable solution in place. But in the case of ARIA? We have no such luck.

What is the result of using both a traditional progressive enhancement approach and ARIA together? Like any other spec, when we use a technology, we expect, generally, to have all, or at least most, of that spec supported. For example, you’d never use CSS for background colors if you didn’t also have support for text color. The same holds true for ARIA—we expect and need to rely on user agents and assistive technology to support all aspects of ARIA. Right now, as the spec is being finished and implementations are still evolving—we simply don’t have that.

For this to work, then, we’ll need to have ways within ARIA of also turning things “off”—of hiding those redundancies that we create by using traditional progressive enhancement alongside ARIA.

As we saw in the required form fields example, our best hope for this at present is a combination of role=“presentation” and aria-hidden=“true”.

Let’s look at one final example—a “tabbed panel” interface.

Without ARIA, we can do a few things to make a tabbed panel work in all scenarios. As part of a progressive enhancement strategy, we would ensure that the content works first with basic functionality as provided by HTML. We would then change the way the tab set looks, and, finally, change the way it behaves.

Here’s a basic strategy:

Without JavaScript, we’d have an unordered list of links on a page that point to a place further down the page that contains the content for the tab. We might even include a heading at the beginning of the content that helps create the association.

Here’s a basic version that shows how we’d support this content without the use of JavaScript.

You’ll see that at the top of the page we have a basic set of links that go to content already contained in the page:

 <li><a href="#first">First Tab</a></li> 
 <li><a href="#second">Second Tab</a></li> 
 <li><a href="#third">Third Tab</a></li>

This takes care of the non-JS scenario. Now that it works without JavaScript, we’ll take this a step further by transforming that same page into a set of tabs. Using some crafty JavaScript, we create a JavaScript enabled version that applies some new styles and specific keyboard functionality.

With JavaScript on, you’ll see that the basic links are transformed. We use this JavaScript to set all of this up with the AriaTabs function. In pseudo-code it looks like this:

  1. Loop through the divs that are being used as tabs,
    1. set class=“tabPanel” so that it picks up the right CSS rules, and
    3. hide the div/tab Panel.
  2. Find the tab list (our navigation) and loop through each one to,

    1. create appropriate id values to tie the controls to the tab,
    3. set up the click for each link so that it changes the current active tab in the tablist, shows the new tab’s content, and hides the old tab’s content,
    5. create the proper keystrokes for the tabs from the DHTML Style Guide (more on this in a moment), and
    7. determine which tab should be showing and add the CSS class=“current” so that it has a different appearance than the other tabs.

You can see the complete function here.

This example works well for keyboard users, and doesn’t require any ARIA for users of assistive technology. Screen reader users will be clicking on links that are within the same page, and that’s exactly what they’re getting. Play with that example and you’ll see that you can use the keyboard to focus on a tab control and use the left/right arrows to switch tabs or you can use the tab keystroke to move to the next tab and hit enter to display the tab.

This is an important piece of the puzzle—without ARIA, none of this will be announced to assistive technology as tabs, so sticking with links as a familiar paradigm is an important step in making this interface make sense.

As a final step, we add support for ARIA by adding ARIA attributes to provide the appropriate programmatic accessibility to assistive technology.

We use the same basic strategy as without ARIA with a few additions to the code:

  1. We loop through the individual tabpanels to:
    1. add ARIA support by giving each a role=“tabpanel”, and
    3. hide it with aria-hidden=“true”.
  2. When we find and create our tab list, we give it a role=“tablist” and loop through each item in the tablist to:

    1. give it a role=“tab” and set aria-selected=“false”,
    3. move up a level in the DOM to the parent and give it a role=“presentation” to remove the semantic meaning conveyed by a list item (why? In the ARIA scenario, the list isn’t meaningful because this is now an ARIA tablist),
    5. programmatically tie the tab and the tabpanel together with aria-labelledby (think of this like a <label> element’s for/id pairing with inputs),
    7. setup the keystroke handlers to change tabs, just as we did without ARIA, but this time, not only do we manipulate the CSS classes, we change ARIA attributes, and
    9. add tabindex=”-1” to the inactive tabs so that we can programmatically focus on them, but they are no longer in the default tab order.

You can see the complete function here.

We’ve done a lot here to set this up with ARIA and the proper keystrokes—you may be wondering, though, what are the proper keystrokes?

For all the details refer to the AOL DHTML Style Guide section on tab panels. This document attempts to codify what keyboard behaviors should exist for a variety of widgets in webpages and applications.

In a nutshell, a tab control (which we don’t natively have on the web) works like this: You use the tab keystroke to focus on the list of tabs available and the currently active tab control gets the focus. We then use arrow keys to switch between tabs and the tab key again to leave the tabbed panels.

Wrapping up the loose ends

One of the most important pieces here is understanding that in the past we’ve usually focused on creating the visual part of the interface. Now, we take what we’ve always been doing with CSS and make it programmatically available to assistive technologies by providing an expanded set of states, roles, and properties that just aren’t available to us in HTML.

So now, when you’re working with an interface, and you see yourself changing class names, what else will you do? Yes, that’s right. Good to hear you say that—you’ll look at adding some ARIA to go along with it.

Simple, right?

Not quite.

We seek a dichotomy of behavior to provide the best interface possible to people that have full ARIA support as well as those that don’t. We set up keystrokes and focus behaviors with JavaScript in both the ARIA and non-ARIA scenario.

We’re changing behavior to match what we want it to be for a set of tabs. But that will only make sense when they can behave as tabs, and that’s when we know we have support for ARIA.

  1. With ARIA support, the links will be announced as a tab when given a role=“tab”. Without ARIA support they’ll be announced as a link, and the user will expect them to behave like a link.
  2. With ARIA support, we use tabindex=”-1” on the inactive tabs (which are actually links). They will work just fine with ARIA support because we’ll be able to programmatically react to those keys that are designed for a tabbed interface. But without ARIA support, by adding tabindex=”-1” to those links, they are no longer available to the user by using a keyboard. We’ve actually taken functionality away.

What of older assistive technology that doesn’t support ARIA? They won’t get any announcement that you’re on a tab, but the JavaScript will be coded such that it assumes you have tab functionality.

And there’s the catch, ladies and gentlemen.

The problem is that we want things to behave differently when ARIA is supported versus when it is not. Why? In a tabbed panel without ARIA, we want them to simply behave as links. In a tabbed panel with ARIA, we want them to behave as tabs.  We want a seamless experience for everyone, ARIA or non-ARIA.

Where does that leave us?

I know that this isn’t every possible example of ARIA—I’ve chosen specific examples to illustrate a point. And don’t get me wrong, I’m a very big believer in ARIA. I teach it in every one of the advanced Ajax and accessibility or ARIA/HTML5/CSS3 workshops that I deliver. I’m sure to point out to every attendee that they need to be watching ARIA and implementing it where it is supported.

The problem that we have right now is that ARIA is an all or nothing deal. And writing scripts that respect both an ARIA supported methodology and a non-ARIA methodology is going to be incredibly difficult, because we have no reliable way of knowing the status of a user agent’s support for ARIA—it depends on something we can’t detect: the right combination of browser, assistive technology, and full ARIA implementation.

Suddenly, our progressive enhancement stack has been broken. We can’t detect ARIA support in the browser and therefore can’t make wise decisions on how to provide the most appropriate keyboard access for ARIA and non-ARIA scenarios.

Here’s what I believe we truly need to get the job done to the best of our professional abilities so that we can use ARIA now and still use the concept of progressive enhancement:

  1. We need to be able to detect ARIA support in the browser, just as we detect for other capabilities.
  2. We need to be able to detect ARIA support in assistive technology so that we know if ARIA is supported in those user agents.

Armed with those two pieces of information we can all create an interface that provides the right behavior in the right situations.

I’ve talked with others about this before and I usually hear something to the effect of “soon enough we won’t have to worry about the non-ARIA scenario because it will be supported in browsers and assistive technology.” We’ll just have ARIA support everywhere. In how many years though? Developers are out there implementing ARIA now because they’ve been told there is support in assistive technology and browsers for it. If they’re using it now, they need to be able to use it properly now.

We live in a world where all user agents simply do not have ARIA support. How long has it taken for IE6 to be a browser that we no longer support? Oh, wait. You mean you still need to support IE6? My point, exactly. We’ll have a non-ARIA scenario to deal with for a long time to come. As such, we need the tools to provide a great experience to everyone.

12 Reader Comments

Load Comments