Expand and collapse content accessibly with progressive enhancement, jQuery, and ARIA

Posted by Maggie on 04/08/2010

Topics:

Collapsible content areas are frequently presented in web sites and applications as a way to let users to control how content is shown or hidden on the page. Also called collapsibles, spin-downs, toggle panels, twisties, and content disclosures, they're ideal for selectively displaying optional information — like instructional text or additional details, for example — so users can focus on the task at hand and view this content only as needed.

The collapsible content area widget is fairly simple — a couple of HTML elements controlled with minimal CSS and JavaScript — but when we were researching our book, Designing with Progressive Enhancement, we discovered that many common approaches to creating collapsible content fail to incorporate accessibility features. Happily, there is a way to build collapsible content with progressive enhancement so it delivers an optimal accessible experience for sighted and screen reader users alike.

View a demo

Before we dive in to the details, let's take a look at the widget in question. Here's an error dialog we created for a photo site, which appears when a photo upload fails — it provides a high-level summary, and a collapsible Details block with supplemental content about which photos failed to upload and why:

Demo page of collapsible content plugin

NOTE on the demo's "View low bandwidth" link: This demo runs on our EnhanceJS framework, which adds a "View low-bandwidth version" link to allow users to toggle from a basic to enhanced view, and drops a cookie on change. If you click the link to view the low-bandwidth version of the demo, you'll need to click it again to view the enhanced version of this site on future views. (Learn more about EnhanceJS here.)

So what's the problem here?

When we first set out to build an accessible collapsible content widget, we thought that the best way to hide the content and keep it accessible to screen readers would be to position it off-screen. We didn't appreciate that while technically this does make the content accessible, positioning content off-screen doesn’t actually provide an accessible experience.

People using assistive technologies like screen readers want to be able to interact with the page just as sighted users do. As accessibility specialist Adam Spencer noted in a recent CBS News story, “True accessibility is giving blind people the same options to access information as sighted ones” (emphasis ours).

While it’s true that hiding collapsible content off-screen at least keeps it in the page and accessible at a baseline level, it’s decidedly sub-par compared with the standard web user’s experience. For example, when a user with a screen reader encounters an accordion widget with ten sections, just like a sighted user they want to hear only the open section, not all content in every section. Screen reader users also expect to use the keyboard to quickly hear the accordion section headings — the screen reader equivalent of quickly scanning the page. When you simply hide content off-screen, these actions aren’t possible; instead, the screen reader reads all content in all the hidden panes, and the screen reader user has no option but to wade through it all in the order it appears in the markup.

Ideally, the experience for both sighted and visually impaired users should be as functionally similar as possible. The W3C Web Content Accessibility Group (WCAG) has outlined a set of four principles that must be met to ensure that content is accessible. Quickly, they say that it should be:

  • Perceivable — Information and user interface components must be presentable to users in ways they can perceive.
  • Operable — User interface components and navigation must be operable.
  • Understandable — Information and the operation of user interface must be understandable.
  • Robust — Content must be robust enough that it can be interpreted reliably by a wide variety of user agents, including assistive technologies.

That’s all well and good, but these definitions are a little abstract. What does it really mean? WCAG provides extended definitions that give us some helpful clues — for example, Perceivable content “can't be invisible to all of their senses”; an Operable interface “cannot require interaction that a user cannot perform”; an Understandable interface’s “content or operation cannot be beyond their understanding”; and it is Robust if “as technologies and user agents evolve, the content should remain accessible”.

In light of these principles, many common techniques for presenting collapsible content fall short. Consider these scenarios:

  • Hiding content by default in the CSS, and relying on JavaScript events to let the user display it. If JavaScript is disabled or unavailable, the user has no means of accessing the content, and may not even know it's there. In this case, content is neither perceivable nor operable — or worse, if the design provides a hint (like an open/close icon or "View details" link), the hint is perceivable but the content non-functioning.
  • Hiding content by positioning it off the page (e.g., position: absolute; left: -9999px;). Doing this ensures that the content is available to screen readers. However, it's always available — the user has no ability to control showing or hiding, or manage whether it's is read aloud. As collapsible content it's not operable; and depending on the widget's content, presenting it all simultaneously may not be understandable.
  • Providing only a visual indicator, like an icon, on the clickable element to show that it can manipulate visibility of related content. This works for sighted users, but fails for screen readers. Unless the icon is accompanied by some adequate auditory feedback that content can be shown/hidden, the feature may not be fully perceivable or understandable to the screen reader user.
  • Applying JavaScript events to a non-natively-focusable element (such as a heading) to show/hide related content. While it works for mouse users, this approach does not guarantee that the widget is navigable for keyboard users (particularly in browsers that don't properly support the tabindex attribute), which are necessary for both screen readers and many mobile devices. In other words, it's potentially neither operable nor robust.

To create a collapsible content widget that works for everyone — and doesn't compromise the experience for screen reader users — we had to rethink what "accessibility" means when showing and hiding content.

Our approach

We start by marking up the page with semantic HTML elements for the heading and content blocks. For example, consider the collapsible widget portion of our error dialog, which consists of a heading element immediately followed by an unordered list:


<div id="message">
	. . .
	<h2>Details</h2>
	<ul>
		<li>purple-flower.tif <em>Not a supported file format</em></li>
		<li>flower-photos.zip <em>Not a supported file format</em></li>
	</ul>
</div>

This markup provides a usable and natively accessible basic experience in all browsers.

If JavaScript is present, we use it to apply enhancements that transform this markup into a functioning collapsible widget with a number of accessibility features. The enhancement script appends several attributes and elements to the basic markup:

  • classes are assigned to the heading element which hide the Details content and apply a visual cue (icon) to indicate that it can be expanded or collapsed
  • a span tag is appended to the heading immediately before the text label which contains the word "Show" followed by a single space if the content is hidden by default; when the content is shown, the script dynamically updates this word to "Hide." This span is intended only for screen readers to audibly describe the heading's function as a toggle link (i.e., "Show Details"), so it's hidden from standard browsers with absolute positioning and a large negative left value
  • a standard anchor link element is wrapped around the heading content to allow it to receive keyboard focus and be accessed with the Tab key
  • an aria-hidden attribute is assigned to the unordered list to ensure that it is truly hidden from ARIA-enabled screen readers when it is hidden from sight (aria-hidden="true"). While display: none; will sufficiently hide the content in current screen readers, future screen readers may not continue to obey visual styles like display, so adding this attribute is considered good, fail-safe practice.
  • classes are also appended to the unordered list to show or hide it visually with CSS. To hide the content, we use the display: none CSS property so that the content is completely hidden from all users

The resulting enhanced markup looks like this:


<div id="message">
	. . .
	<h2 class="collapsible-heading collapsible-heading-collapsed">
		<a href="#" class="collapsible-heading-toggle">
			<span class="collapsible-heading-status">Show </span>
			Details</a>
	</h2>
	<ul aria-hidden="true" 
		class="collapsible-content collapsible-content-collapsed">
		<li>purple-flower.tif <em>Not a supported file format</em></li>
		<li>flower-photos.zip <em>Not a supported file format</em></li>
	</ul>
</div>

And the enhanced classes for providing feedback are structured as follows – notice that we use a single background image sprite (icon-triangle.png), and simply adjust the background position to show the appropriate state:


. . .
.collapsible-heading {
	padding-left:15px;
	background:url(../images/icon-triangle.png) 0 6px no-repeat;
}
.collapsible-heading-collapsed {
	background-position:0 -84px;
}
.collapsible-heading-toggle {
	text-decoration:none;
	color:#333;
}
.collapsible-heading-status {
	position:absolute;
	left:-99999px;
}
.collapsible-content-collapsed {display:none;}
. . .

NOTE on testing browser capabilities: To ensure that the browser can fully support all enhancements, we recommend using our EnhanceJS framework, which tests browsers' CSS and JavaScript capabilities and applies enhancements only after those tests are passed. Read more about EnhanceJS here: Introducing EnhanceJS: A smarter, safer way to apply progressive enhancement.

What we accomplish

When built as described above, our collapsible content widget incorporates specific features to meet the four accessibility principles outlined by WCAG:

  • To ensure that it’s perceivable and understandable, it includes cues for both visual and screen reader users that indicate the heading element will show and hide associated content. Visual users see an icon that changes orientation based on the content's state; for screen reader users, the script conditionally appends the word "Show" or "Hide" before the heading text to describe the content's state and provide appropriate language to indicate how the heading is used. Additionally, the widget provides feedback to ARIA-enabled screen readers with the ARIA attribute, aria-hidden; it's set to true when the content is hidden, and false when shown.
  • To make it operable, we ensure that the widget is fully navigable and controllable with the keyboard. When CSS and JavaScript enhancements are applied to the page, the script wraps each heading element's text with an anchor element so that the user can navigate to and focus on the heading with the Tab key, and hide or show related content with the Enter key.
  • And our progressive enhancement approach helps us ensure it’s robust: The foundation is semantic HTML, which is accessible and usable on all browsers; only when a browser is capable of enhancements is the markup transformed into a collapsible widget with enhanced styling and behavior.

Looking ahead

The HTML5 spec includes a new element called details, which if implemented as proposed, will provide a native collapsible widget that will require no JavaScript and will be accessible without requiring any additional work. No browsers have adopted the details element at this time, but it does present a potential alternative to this approach that we may be able to use in the future. Bruce Lawson provides an interesting summary of details and an argument in favor of using semantic interactive elements over JavaScript.

How to use the plugin

Download the collapsible content plugin script (see instructions below), and reference it and the jQuery library in your page. You'll also need to append the CSS classes listed above for the collapsed state of the widget. NOTE: You'll likely need to edit this CSS to fit with your own project's design.

Call the collapsible plugin on the heading or other element that you're using to show related content; the plugin then assumes that the next element in the source order is the content block to show/hide. For example, on DOM ready you can call the plugin on all H2 elements:


$(document).ready(function(){$('h2').collapsible();});

Download (and help us improve) the code

The collapsible content plugin code is open source and available in a git repository, jQuery-Collapsible-Content. If you think you can help on a particular issue, please submit a pull request and we'll review it as soon as possible.

If you've already purchased Designing with Progressive Enhancement, you can download all twelve widgets at the code examples download page.

Code update!

James Frank extended the script to include the following:

I simply added a couple of tweaks. First, the ability to expand and collapse the content by using the right and left arrow keys, respectively, when focused on the trigger element. Previously you could only do this by using the enter key (which is equivalent to a click). Second, the ability to start the element collapsed instead of expanded. You can do this by passing the value “true” to the function when initiating the collapsible section. from his blog post, Improvement to Filament Group’s Collapsible Script

Thanks, James!

Book cover: Designing with Progressive Enhancement

Enjoy our blog? You'll love our book.

For info and ordering: Visit the book site

Comments

I think this is a nice little plugin. It’s nice to see people going the extra steps to provide functionality and usability to everyone.

Comment by Jeff on 04/20  at  06:29 PM

The (position: absolute; left: -9999px;) advice is not correct for RTL layouts. IE shows a huge horizontal scrollbar to nowhere. top: -9999px gives consistent behavior for LTR and RTL layouts.

Comment by Felix on 04/23  at  04:59 PM

If you double click the icon or “Details” the resulting animation looks awful… and there should be a hand icon when hovering over the icon (but that’s an easy fix in CSS)

Comment by Marcus Tucker on 04/23  at  08:37 PM

@Felix: That’s an interesting point, thanks. I guess in an RTL layout, you could use right: -999px; instead too? You’re right that in this case the top property could be used to hide the text in a way that works in both directions, you’d just need to be sure it’s a large enough number to truly hide it regardless of where it is on a tall page (or maybe hidden overflow could help with that). Side note: hiding with the top property isn’t always a good idea (see article http://www.webaim.org/techniques/css/invisiblecontent/). Thanks for the feedback.

@Marcus: Thanks. You make a good point about the hand cursor (we’ll add that to the CSS). As for the double-click issue you mentioned, it looks like adding overflow: hidden; to the .collapsible-content UL may fix it.

Comment by Scott (Filament) on 04/23  at  09:11 PM

Interestingly enough is that right: -99999px; also gives the horizontal scrollbar to nowhere.

Comment by Felix on 04/23  at  09:20 PM

@Felix: would you mind linking up a demo for us? Also, did you try setting overflow: hidden on the heading? Maybe that will help regardless of direction…

Comment by Scott (Filament) on 04/23  at  09:28 PM

@Scott - Sorry, I didn’t mean to sound so negative in my earlier comment, I posted in a hurry… otherwise it’s a very good implementation, not enough jQuery plugins & scripts pay attention to semantics, accessibility and progressive enhancement principles…

Comment by Marcus Tucker on 04/24  at  02:21 PM

What a verbose and overly-hyped way to say: add a “hide” class to items and use jquery onload to hide them. Sheesh.

Comment by ziggy on 04/26  at  06:13 AM

@Ziggy: The approach described in this article is a bit more involved than what you’ve described. You can easily show or hide content in one or two lines of code with jQuery, but doing so in a way that’s accessible to users on screen readers involves additional work.

In the example you gave, simply adding a “hide” class may allow you to hide some content without using inline CSS, but it does nothing to make the content discoverable/perceivable/understandable/operable to users with assistive technology.

If you’re interested in the accessibility benefits of this approach, read the portion above where we describe the addition of the “Show” and “Hide” text. In our example, this additional text provides affordance to non-sighted users that there’s additional hidden content to be expanded (for sighted users, the arrow icon serves the same purpose).

Comment by Scott (Filament) on 05/04  at  09:59 PM

nice demo plugin

Comment by manjuv on 06/07  at  04:58 PM

i’ve just finished local testing of this (with slight modifications to the CSS and the example.js); i wonder whether there’s any documentation for options that can be set on the widget, e.g. animation effects etc, like the jQuery accordion’s autoHeight option or the bounceslide option.

plan to implement this on my site today! thanks for the great widget.

--cz

Comment by seezee on 06/08  at  05:55 PM

I modified the plugin’s behavior slightly for use on my site; it now auto-collapses all open folds when a new fold is expanded. Here’s the (relevant) modified code:

.click(function(){
    
if( $(this).is('.collapsible-heading-collapsed') ){
    
//next 3 lines added by cjz to close expanded item when new item selected
    
var collapseAll = $('.collapsible-heading');
    for (var 
0collapseAll.lengthi++);
    
collapseAll.trigger('collapse');
        $(
this).trigger('expand'); 
    
}    
    
else {
        
$(this).trigger('collapse'); 
    
}
    
return false;
})
.
trigger('collapse');

Comment by seezee on 06/14  at  05:30 PM

I am thinking of trying to modify this plugin to use the jquery-ui themeroller styles, instead of custom content.  Are those patches you would be interested in?  Or, I guess, since you’ve released under MIT License (thanks!), nothing stopping me from distro’ing a modified version under same license, cool. But I’ll see where I get, and would be happier to see it go back in the ‘main’ version of this, if you’re interested.

Comment by Jonathan Rochkind on 10/06  at  10:11 PM

I would like to link to a page of content that is using this collapse content script, but I want to target an ID on the page which is normally collapsed and have it automatically be open when the page is loaded. Anyone got the solution?

Comment by Pat Rees on 10/14  at  11:43 PM

Use an anchor fragment, like <a href="#foo" rel="nofollow">, then modify the code so that if the url contains the fragment, the section is expanded. I just did a Google search & found this: http://iwebdevel.com/2009/06/10/javascript-get-anchor-from-url/

Haven’t tested it, so good luck!

Comment by seezee on 10/15  at  12:00 AM

Nice writeup!

I think it would be more semantic from a CSS point of view to set the arrow icon as a background on the anchor element classed with “collapsible-heading-toggle”. The padding could go there as well, and that would also render the cursor:pointer set on the heading useless.

Also, the text “Show/Hide details” is not a very descriptive text read out of context to a blind user “scanning” the page through tabbing. It doesn’t tell us what kind of details the link is showing. Don’t know what level of support screen readers have for something like the title attribute, but maybe we could place that extra descriptive piece of text inside of there. Something like title="File upload issues”.

Maybe this isn’t a big deal after all. I guess the experienced screen reader user is quite used to stuff like this on the web - I mean, just look at all those “Read more” links that you find everywhere…

Comment by Anders on 11/02  at  10:10 PM

Here is a slightly tweaked version of jQuery.collapsible.js, intended for use on a site already using ThemeRoller.

1) It requires no special CSS or images, just the .js file defining the plugin is enough to give you the behavior.
2) It uses the ThemeRoller triangle icons for icons, to accomplish #1 (one other tweak goes into #1 too).

https://gist.github.com/661822

(Sorry if this is a double-post, I got no confirmation of any kind when I tried to submit this before maybe the github URL makes the server mad, but trying again.)

Comment by Jonathan Rochkind on 11/04  at  12:45 AM

I think that it would be better if show / hide text was an image with the alt text “show” or “hide”. The icon is not only decorative since it means for sighted users “show / hide”, so if images were not loaded, this information also dissapears using this method.

Comment by kcmr on 11/24  at  07:55 PM

Using this plug in a couple of projects right now. Would be good if there was a way to set the first piece of collapsed content to show on page load and then only hide it when you click on the heading instead of having them all hidden to start with

Comment by Andy Brooks on 12/15  at  07:22 PM

Andy, you can easily do that yourself just by saying so in jquery, with the existing widget.  After you apply the collapsible to the elements you want, everything will be hidden, but just immediately open it up with your own code:

$("something").collapsible().trigger("expand");

Or

$("bunch of somethings").collapsible();
$("selector to find the first one").trigger("click");

I thought that calling trigger("expand") would work in the second case too, but the “expand” (or “collapse") event only works on the actual ‘collapsible’ object, not on the element you applied collapsible too, and I am not sure if there is a way to access the actual collapsible element after you’ve already applied it, I think calling collapsible() again will kind of double-apply it, with unpredictable results. Could be wrong though. If the original author has any guidance, appreciated.

Comment by Jonathan Rochkind on 12/15  at  07:34 PM

Refreshing the browser the opened content is visibly for a short time and then begins the closing of the opened area - is it possible, to prevent this and to open the content only by clicking the details link?

With best regards from Austria

Comment by Hubert on 01/07  at  07:49 PM

@Hubert—I was wondering the same thing.  Got really excited when I found this, but that issue may keep me from being able to use the plugin.

Comment by Liv on 04/28  at  09:19 PM

Commenting is closed for this post.

Book cover: Designing with Progressive Enhancement

Enjoy our blog? You'll love our book.

For info and ordering: Visit the book site