Valid and Accessible Collapsible Panels with Scriptaculous

Believe it or not, creating collapsible panels — and “rich web elements” in general — requires some careful thought and planning. Some things need to be taken into account, such as:

  • Not all users have mice – some only have keyboards (or equivalent)
  • Some people don’t have JavaScript – whether by choice or necessity
  • Some people don’t have CSS support

The only thing you can really bank on is that your bare markup will be handled OK, so it’s important to start with that and make sure everything on top of that only acts as candy; eye or otherwise.

View the collapsible panels example.

So with our example, we’ll start by designing the markup we need as simply as possible. We’ll want a container for each panel, and each panel will have a heading and another container for content. Something like this:

<div class="collapsible">
    <h2>Collapsible Panel</h2>
       <p>A paragraph of content for the panel</p>

Now some JavaScript magic needs to happen. We’ll need to pollute our markup slightly, but it’s worth it. Give each collapsible panel an id, and just inside the panel content div we want to call a JavaScript function with the panel that will:

  1. Hide the content and add a ‘closed’ class to the panel.
    (We hide it in JavaScript so non-JavaScript users can still see it)
  2. Inject a link into the h2 like so: h2.innerHTML = '<a href="#">' + h2.innerHTML + '</a>';
    (This way non-JavaScript users won’t have useless links sitting around)
  3. Get the new link and set its onclick

        function attachCollapsible(collapsible)
            var div = $(collapsible);
            var heading ='h2')[0];
            var contents ='div')[0];
// initially hide it div.addClassName('closed'); contents.hide();
// add an anchor at run time, // that way non-js users won't see anchor, but js users will // still be able to use their keyboard for it. heading.innerHTML = '<a href="#">' + heading.innerHTML + '</a>'; heading.firstDescendant().onclick = collapsibleOnClick(div, heading, contents); }

The onclick should:

  1. Clear the contents’ height to make sure the effects aren’t in a half-baked state: = '';
    (There might be a better way to do this, but it’s simple and works well enough)
  2. Check whether the panel has the ‘closed’ class
  3. Fire up an appropriate effect
  4. Toggle the ‘closed’ class
function collapsibleOnClick(div, heading, contents)
    return function() {
        // clear the height; gets messy otherwise if
        // a blind is already in progress
        // not perfect, but simple and huffy enough,
        // and doesn't seem to be able to get into a bad state = '';
// do we need to go up or down? if (div.hasClassName('closed')) { new Effect.BlindDown(contents,{duration:0.3, fps:100}); Element.removeClassName(div,'closed'); } else { new Effect.BlindUp(contents,{duration:0.3, fps:100}); Element.addClassName(div,'closed'); }
// event has been dealt with. return false; }; }

And hey, you’ve now got a collapsible panel that works with and without JavaScript, works with a keyboard, has no useless hash links and has nice clean markup.


  1. [...] be writing all my dev stuff on KleeneCode (do you get the pun?) starting with an article on accessible collapsible panels. amospheric will be free then to release all inhibitions and create more offence than ever, and [...]

  2. found your site on today and really liked it.. i bookmarked it and will be back to check it out some more later ..

  3. First collapsible Panels with clean Xhtml code! Great stuff -thank you

  4. Is it possible to nest these panels? So a panel opens a set of panels which can also be opened?

  5. Very nice. I will give it a try. I’m teaming up with someone on a project, and our current panels (created in a different way) don’t seem accessible. We applied FANGS, Mozilla’s screen reader emulator, to check what we now have, and only the open panels are accessible. I’m looking forward to trying your solution. Thank you!

Leave a Reply