/wakamoleguy

A Basic Vanilla Accordion List

Here lies my first contribution to the Vanilla Web Diet. The Diet has gotten quite a following recently, and it even has a book coming out about it. I first coded up this accordion list several months ago as part of the GetOnSIP project. Since then, I've tried several times to write up a complete post about its inner workings. And I've failed. I've realized that there are really many different things to consider, from the HTML structure to the CSS effects, to the actual behavior of the list. No one solution is right for everybody.

Rather than give up, I decided to break it up into two posts. In this one, I'll show off a very basic, almost primitive accordion list. In a future post, I will go over many different behavioral, semantic, and stylistic directions to take to customize an accordion list to fit your exact needs.

In the end, the goal is not to create a one-size-fits-all widget that is perfect for everybody. That is the kind of goal best suited for frameworks. The goal is to show what is possible with vanilla JavaScript, because sometimes you don't need to download an extra 80K just to make a section of your site collapsible.

Anyways, enough of that. Here we go.

The HTML

A lot of different structures could be turned into an accordion list. From jQuery UI's unsemantic <div>s and <span>s to ordered or unordered lists to even <dl> definition lists, just about anything can play the role of a collapsible set of content blocks. For this basic accordion, I chose to use HTML5 <section> elements, as I felt it made the example the cleanest.

<section class="accordion">
  <section>
    <h1>Yesterday</h1>
    <p>I was sick yesterday, so not much happened.</p>
  </section>
  <section>
    <h1>Today</h1>
    <p>I have a bunch of things to do today:</p>
    <ul>
      <li>Grocery shopping</li>
      <li>Stop at the bank</li>
      <li>Dinner and a movie</li>
    </ul>
  </section>
  <section>
    <h1>Road Blocks</h1>
    <p>I'm sick!<p>
  </section>
</section>

See the Pen Vanilla Accordion by Will Mitchell (@wakamoleguy) on CodePen.

There's not much to see: a section to become an accordion and three sub-sections to hide and show. I've included a nested list just to prove that the accordion is fairly robust in what content it can show. The outer <section> gets the class accordion, but otherwise there is no extra markup. Remember that if JavaScript is disabled or fails to load, the user will still see the HTML. Keep it clean.

The JavaScript

The most basic accordion has very simple behavior. In the beginning, all list items are collapsed; only the heading content is visible. On interacting with the heading (click or touch, for example), the other content in the list item expands out and becomes visible, at the same time collapsing any other items. Here is the code, followed by an in-depth explanation.

(function () {
  var accordions, i;

  // Make sure the browser supports what we are about to do.
  if (!document.querySelectorAll || !document.body.classList) return;

  // Using a function helps isolate each accordion from the others
  function makeAccordion(accordion) {
    var targets, currentTarget, i;

    targets = accordion.querySelectorAll('.accordion > * > h1');
    for(i = 0; i < targets.length; i++) {
      targets[i].addEventListener('click', function () {
        if (currentTarget)
          currentTarget.classList.remove('expanded');

        currentTarget = this.parentNode;
        currentTarget.classList.add('expanded');
      }, false);
    }

    accordion.classList.add('js');
  }

  // Find all the accordions to enable
  accordions = document.querySelectorAll('.accordion');

  // Array functions don't apply well to NodeLists
  for(i = 0; i < accordions.length; i++) {
    makeAccordion(accordions[i]);
  }

})();

See the Pen Vanilla Accordion by Will Mitchell (@wakamoleguy) on CodePen.

Okay, so there's a lot going on in just a few lines of code. Let's break it down.

Short Circuit When Unsupported

We depend on the browser having implemented element.classList (Can I use...) and Document.querySelectorAll (Can I use...). If they don't have those, the accordion won't work. Rather than crash, we detect this and exit cleanly.

Find Things Once

DOM access is slow. We deal with this by fetching everything we need up front and caching it for later. This especially shines when clicking to expand a new section. We could search the sections siblings for any expanded sections, but that is slow and tedious. Instead, we hold on to the currently expanded section so we have it ready to go. And when we do click a new section, all we have to do is swap a couple classes and update the variable. Easy-peasy!

Some of you may be saying "But hey! Aren't you doing a whole bunch of querySelectorAll calls? Doesn't that traverse the DOM way more than it needs to?" Umm...yeah. A clever person could find a way to traverse the DOM once, looking only for accordion sections' heading elements an inferring the outer accordion from there. Perhaps that will make a return visit in another post. For now, I'm satisfied not having to go back to the DOM after initialization.

Mark Working Accordions

accordion.classList.add('js');

This may seem unnecessary, but it's imperative to keeping the accordion accessible. Remember that for modern browsers which can support the accordion, we would like to start with the headers collapsed. And yet older browsers should see expanded content. We accomplish this by adding the .js class to the accordion. This marks it as enabled and provides a CSS hook to style on.

The CSS

This is where our hard work in JavaScript land will pay off. By adding relevant classes to a semantic HTML structure, the CSS involved becomes fairly simple. I left off vendor prefixes. Browsers that don't support the full transition effect will gracefully degrade to switching instantly between visible and invisible.

.accordion.js > * {
   overflow: hidden;
}

.accordion.js > *:not(.expanded) > *:not(h1) {
    max-height: 0;
    margin-top: 0;
    margin-bottom: 0;
    opacity: 0;
    visibility: hidden;
}

.accordion.js > .expanded > *:not(h1) {
    max-height: 10em;
    opacity: 1;
    visibility: visible;
}

.accordion.js > * > h1 {
    cursor: pointer;
    visibility: visible;
}

.accordion.js > * > *:not(h1) {
    transition:
        max-height 1s,
        visibility 1s,
        margin 1s,
        opacity 1s;
}

See the Pen Vanilla Accordion by Will Mitchell (@wakamoleguy) on CodePen.

Most of the magic here is hidden in transition the max-height of non-heading children of collapsible content. Ideally, we would like the content to transition from height: 0 to height: auto, but animating to the automatic height of an element isn't possible. Instead, we overshoot with max-height: 10em. The 10em will vary depending on the content; you must pick a value bigger than the content will every display (so as to avoid trimming), but overshooting by too much will cause odd animation timing.

We also strip the vertical margins of collapsed content while they are collapsed, which prevents weird spacing issues. I also like to add an opacity shift, too, to get a fade effect.

Finally, not that each style is protected by the .accordion.js selector, so they will only apply to enabled accordions. Users in older browsers will be shown a block of content with only default styling.

Conclusion

It certainly isn't a silver bullet, but in just 33 lines of (commented, spaced) JavaScript and even less CSS, you can get a nice accordion effect on your page. Here's the final result:

See the Pen Vanilla Accordion by Will Mitchell (@wakamoleguy) on CodePen.

So next time you are creating a web site or web app, don't just run to a bloated UI library. Instead, take a few minutes to assess the situation and decide what you really need. Bytes do affect performance, and sometimes less really is more. You, too, can help get the web back on its vanilla diet.

Tune in next time for some more options on customizing accordions, including different HTML structures, keyboard access, and more!

Cheers!