WWW FAQs: How do I create a collapsible outline?


2007-04-05: Outlines are a useful way to represent information. But displaying the entire outline at once can be confusing. It's too hard to grasp everything at once. Wouldn't it be nice to show just the top-level headings at first, and allow the user to click the mouse (or use the keyboard) to explore the levels beneath?

Of course, it's possible to do this using ordinary hyperlinks:


<ul>
<li><a href="i.html">I. Part One</a></li>
<li><a href="ii.html">II. Part Two</a></li>
</ul>

In this simple design, we link to separate pages for the two main headings of the outline. Each of those pages would then link to additional sub-pages, and so on.

"Hey, I just want the code!"

No problem! If you like, simply skip to the end of the article and you'll find simple instructions for creating fully collapsible outlines. If you do choose to keep reading, you'll learn how it all works. The choice is yours!

Unfortunately, while this approach does work, it's rather slow. Each time the user clicks, they then have to wait for a new page to be loaded. And a great deal of information is repeated on each page in order to show the user where they are in the overall outline.

is there a better way? Fortunately, there is! We can create a fully collapsible outline that allows the user to open and close the list beneath each heading. We'll do this with dynamic HTML, also known as DHTML. The term "DHTML" refers to the things that can be accomplished using JavaScript, cascading style sheets, and the document object model that allows the two to work together.

The Accessibility Problem

A naive solution based on DHTML wouldn't take long. But we can't ignore the fact that not every user has JavaScript turned on in their browser. Some workplaces mandate that JavaScript be turned off, just in case a JavaScript security problem is discovered. And other users may be using browsers for the blind, or navigating with the keyboard or another alternative to the mouse. All three of these issues can create problems for a poorly designed collapsible outline.

So how do we cope? My solution begins with a perfectly normal nested list, like this short outline for a presentation regarding a chain of ice cream parlors:


<ul>
  <li>Flavors
    <ul>
      <li>Strawberry</li>
      <li>Chocolate</li>
      <li>Coffee</li>
    </ul>
  </li>
  <li>Locations
    <ul>
      <li>Beach</li>
      <li>Main Street</li>
      <li>Airport</li>
    </ul>
  </li>
</ul>

So far we have a normal, fully expanded outline. And the advantage of this is that if the user has JavaScript turned off... they still get a normal, fully expanded outline! While it is not as convenient as a collapsible outline, it is always acceptable. No user is prevented from taking advantage of the page.

But of course we'd like to do better when JavaScript is available. So let's begin by giving the top level ul element a CSS class name we can recognize:


<ul class="outline">
... etc ...
</ul>

Can we finish the job entirely with CSS? Unfortunately, no. While something resembling an outline could be created with the CSS hover attribute, this would require the user to keep the mouse over the parts of the outline they want to see. If the user wants to expand several portions of the outline at once, they're out of luck. And trying to understand the information about the outline while worrying about the mouse leaving the "hot zone" and closing the outline is never pleasant.

So JavaScript is necessary. But how does JavaScript help us turn the simple fully expanded outline above into an interactive, collapsible one? I'll show you exactly how. But first, you'll need to understand how to hide the collapsed elements of the outline in the first place.

Hiding elements with the display CSS property

Before we can expand and collapse our outline, we need to hide the elements that should be collapsed at the beginning. Fortunately, this isn't difficult. CSS provides us with the display property, which decides whether an element should actually be part of the page layout or not.

It is important to understand that the display and visibility properties are different. visibility: hidden prevents the user from seeing the element, but it still occupies space in the page. That's not right for a collapsible outline— we don't want giant blocks of white space! Instead, we use display: none, which ensures that the element is not part of the page layout at all. And when we're ready to bring the element back, we'll set it to display: block, which allows that ul element to be laid out in the normal way.

Here is an example of a sublist that is currently collapsed (not displayed):


<ul style="display: none">
  ... List items go here ...
</ul>

So, should we do this in advance in our nested list? We could... but if we did, users without JavaScript would never see anything but the top-level entries in the outline. They would have no way to set them back to display: block. So instead of doing this directly in our HTML, we'll let JavaScript do it once the page is fully loaded.

This has the minor side effect that the entire expanded outline may momentarily appear on the screen before it collapses. But if you can't live with this behavior, there's an easy solution: just set display: none on any nested <ul> elements. Then, provide the entire outline again inside a noscript element in your page, so that users without JavaScript can still see that content. Or include a link to an alternative non-JavaScript solution in that element.

If you are not willing to make the effort to do that, then I strongly suggest you follow my suggestion and allow the collapsed part of the outline to be hidden after the page loads.

Responding to onClick events

Now we understand how to hide the nested lists. But how do we bring them back again? JavaScript and the DOM provide us with a wonderful tool known as onClick. Providing an onClick attribute for an HTML element allows us to call JavaScript code whenever the element is clicked on with the mouse. And if the element is a link ( the <a> element), then the onClick handler also responds when the user activates the link with the keyboard.

Here's an example of an li element with an onClick handler:


<li onClick="outlineClick()">Item One</li>

This allows the user to click anywhere in the list item text. As I mentioned, however, only link elements will fire an onClick handler when the user navigates with the keyboard instead of the mouse.

So, here is an alternative that ought to allow keyboard navigation:


<li><a onClick="outlineClick()" href="#">Item One</a></li>

Unfortunately, there is a problem with this approach. When the outlineClick function is called, it has to know which sublist needs to be expanded! How do we get that information?

The answer depends on the web browser being used. In Internet Explorer, we can figure out which element was clicked on by looking at window.event.srcElement. In all other browsers, our outlineClick function will receive an Event object as a parameter, and we can look at the target property of that object to figure out what was clicked on.

This works fine for mouse clicks. But what about links followed via the keyboard? You would be forgiven for expecting that these onClick calls would also receive an event object. That would certainly make sense. But unfortunately it doesn't happen.

How can we solve the problem? Instead of inferring which link was selected, we'll have to pass that information ourselves when we create the onClick attribute for the link. And that brings us to the big question.

Bringing It To Life Without Ugly Markup

Does all of this mean we'll have to fill our elegant nested list with complicated JavaScript and CSS code? Not at all! Or at least, not by hand. We can take advantage of the power of the DOM to automatically locate any outlines in the page, adding the special link elements and JavaScript handlers we need "on the fly." The HTML in our pages stays simple. All we need is a single call to outlineInit() in the onLoad attribute of the body element. And then the magic kicks in.

But how does the magic work? outlineInit begins by fetching a list of elements in the page that have the outline class:


var elements = outlineGetTopLevelLists();

This is why only the top-level ul elements in the page should have the outline class. Don't give the outline class to nested ul elements, please!
The outlineGetTopLevelLists function works by examining all of the ul elements in the page via the DOM's document.getElementsByTagName function, looking for those that have a className containing outline.

Next, the code calls outlineInitOutline to look at each individual top-level outline in the page (which allows you to have more than one). This function is simple: it fetches the list's child nodes via the childNodes property of the outline element, and then looks for children with a node name (HTML element name) of LI. These are the individual list items.

Now outlineInitItem is called. This function does most of the work. The code begins by examining each of the child nodes of the list item, looking for nested ul elements (lower levels in the outline). Each time one is found, it is hidden by setting its style.display property to none. This is how we change a CSS style "on the fly" in JavaScript.

After hiding the nested list, we call outlineInitOutline on the nested list, so that all of the children of that list can be hidden... and so on, as many levels down as necessary.

As we go along, we build an array of the li elements that turn out to have nested lists beneath them. But why is this useful? You're about to find out!

Solving the "Why Are We Here?" Problem

Remember the problem we had with the onClick handler? When the user makes their choice via the keyboard, the onClick handler doesn't know which element was chosen. What we need is a way to uniquely identify the clickable items (the "outline" elements), so that we can pass that unique identifier to our onClick handler.

By building an array of outline elements (the li elements that have lists nested beneath them), we give each outline element an identifier (its index in the array). That way, when we create a link element on the fly and give it an onClick handler for keyboard events, we can include the identifier right in the original HTML for the onClick attribute.

Elegant, isn't it. Here's the code:


var len = outlineItems.length;
outlineItems[len] = item;
var span = document.createElement("span");
span.innerHTML = "<a href='#' " +
  "onClick='outlineItemClickByOffset(" + len +
  "); return false' " +
  "class='olink'>" +
  "<img class='oimg' alt='Open' src='oopen.png'></a>";
item.insertBefore(span, kids[0]);
item.onclick = outlineItemClick;

What's happening here? First, we check how long the outlineItems array already is. That value will be the index for the next item— the new outline item we are processing now.

Next, we add the new item to the outlineItems array and get ready to create a keyboard-friendly link element, containing a "plus sign" image that the user can select with either the keyboard or the mouse.

You might wonder why I don't simply do this by changing the innerHTML property of the list item, like this:


item.innerHTML = "<a onClick='et cetera...'>" +
  item.innerHTML + "</a>";

While this looks like a quick and dirty solution, it has a fatal flaw. When we modify innerHTML for the item, we recreate all of the HTML elements beneath it. And that has two big problems:

1. It is potentially slow if the outline is large and complicated.

2. Any outline elements beneath this level that we have already stored in the outlineItems array get replaced... which means the entries in that array are no longer valid.

We could get around this by creating the link and image elements purely using DOM functions like document.createElement and document.setAttribute, but frankly... it's a pain. And more importantly, Internet Explorer won't play along. An onClick handler created with the setAttribute function simply didn't work in my Internet Explorer tests.

Fortunately, we can work around it by creating a span element with document.createElement, then tucking the link and image elements inside it by setting span.innerHTML. Then we insert the span element at the beginning of our list item with the item.insertBefore method, which does not ruin our patiently collected outlineItems array. This approach keeps our code simple... okay, relatively simple... and avoids the Internet Explorer bug.

We're nearly there— just a few details left! We don't want to put the entire contents of the list item inside a link element, because that would break any hyperlinks in our outline. ButiIt would still be nice to let the user click on the text if they wish, rather than on the "open/close" images. And we can still do that.

As I mentioned, we can't call setAttribute to add an onClick handler in Internet Explorer— it just doesn't work. But we can set item.onclick to any function we like, and that works in both browsers. What it doesn't do is give us an opportunity to pass the ID of our list item.

Fortunately, this time we are talking about a mouse event for certain. So we can set our onclick handler to a function that relies on Event.target or window.event.srcElement to figure out what is happening. See outlineGetTarget for the gory details.

When It's Time To Grow... Or Shrink

We've created a fully collapsed outline (except for the top level, of course). And we've found a way to know which outline element has been clicked upon or selected with the keyboard. Now, how do we finish the job by responding to that event?

The outlineItemClickBody function does the work. Here we fetch the child nodes of the outline element, sort through them to find nested lists, and toggle their style.display properties between none and block.

But how do we make it clear to the user that the outline has been expanded? The user will probably see the nested list underneath, of course, but we can do even better by changing our "open" icon to a "close" icon. We do that by fetching the image element within the link and changing its src and alt attributes on the fly.

Of course, "fetching the image element within the link" is easier said than done. To see how, just look at the outlineGetDescendantWithClassName function. This function, which is similar to outlineGetTopLevelLists, walks through the child nodes beneath the outline element. Each one is checked to see whether it is a member of the oimg class. If not, we check the children of that element before moving on... and so on. We keep going until the first matching element is found and returned.

I could have written this function to simply return the first grandchild of the outline element, but that approach is very brittle— what if you decide to make a seemingly minor change to the appearance of my links, and the img element is no longer the first grandchild? By locating the correct image element in this way, the code becomes more tolerant of changes.

The Devil Is In The Details

That's it... almost! There are one or two important details. The nastiest of these is event propagation.

When an onClick handler for a list item is nested inside another list item... which has its own onClick handler... which handler is actually called? The answer: it depends. But browsers do propagate the event to other handlers beyond the innermost item the user thinks they have selected. And that causes outlines to open and then immediately close again, or behave in other undesirable ways.

Fortunately, Internet Explorer and other browsers both allow us to prevent this behavior. In Internet Explorer, we do it by setting the cancelBubble property on the event. In other, more standards-compliant browsers, we do it by calling the evt.stopPropagation method of the event. You can find this code in the outlineGetTarget function.

A second, less crucial issue is the mouse pointer. When the mouse pointer is over a link or other interactive item, it should change its appearance, but this is not always automatic. We solve this problem by setting the style.cursor property to pointer on all of the list items that contain nested lists.

Enough Talk, Just Give Me The Code!

Of course! Here's how to get the job done:

1. Check out the live demo to see what you'll be getting. My example outline is a small one, but yours can be much more complicated.

2. Download outline.zip and extract it to your hard drive.

3. Upload outline.js, outline.css, oopen.png and oclose.png to your website. Do not upload test.html, that's just an example for you to learn from.

4. Add a link to outline.css in the head element of your page:


<link href="outline.css" rel="stylesheet" type="text/css">

Note: if outline.css is not in the same folder with the page, make sure you change the href appropriately.

5. Load outline.js in the head element of your page:


<script src="outline.js">
</script>

Of course, you must change the src attribute if outline.js is not in the same directory.

6. Call outlineInit() from your onLoad handler:


<body onLoad="outlineInit()">

If you need other onLoad handlers, just separate the function calls with semicolons.

7. Write a perfectly normal nested list— but make sure the top-level ul element, and only that element, is a member of the outline class:


<ul class="outline">
  <li>Flavors
    <ul>
      <li>Strawberry</li>
      <li>Chocolate</li>
      <li>Coffee</li>
    </ul>
  </li>
  <li>Locations
    <ul>
      <li>Beach</li>
      <li>Main Street</li>
      <li>Airport</li>
    </ul>
  </li>
</ul>

8. That's it— really! Refresh your page in the browser. If you have followed these instructions correctly, you now have a fully collapsible outline that works in all major browsers, falls back gracefully when JavaScript is not available, and supports keyboard as well as mouse-based navigation.

Conclusion

Collapsible outlines are a useful way to cope with large amounts of information. However, naive implementations of collapsible outlines create problems for many users, including those in JavaScript-free environments and those with disabilities. My solution copes more gracefully with these situations, and also makes it easy to create outlines by using ordinary HTML in an intuitive way. And if you didn't collapse while reading this article, you have also learned quite a bit about DHTML and CSS. Happy outlining!

Legal Note: yes, you may use sample HTML, Javascript, PHP and other code presented above in your own projects. You may not reproduce large portions of the text of the article without our express permission.

Got a LiveJournal account? Keep up with the latest articles in this FAQ by adding our syndicated feed to your friends list!