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

April 2020 note: Hi! Just a quick note to say that this post is pretty old, and might contain outdated advice or links. We're keeping it online, but recommend that you check newer posts to see if there's a better approach.

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.

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">
    . . .
        <li>purple-flower.tif <em>Not a supported file format</em></li>
        <li>flower-photos.zip <em>Not a supported file format</em></li>

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>
    <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>

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 {
    background:url(../images/icon-triangle.png) 0 6px no-repeat;
.collapsible-heading-collapsed {
    background-position:0 -84px;
.collapsible-heading-toggle {
.collapsible-heading-status {
.collapsible-content-collapsed {
. . .

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.

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:


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.

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.

Thanks, James!

All blog posts