Variations on a Vanilla Accordion List
In the last post, I walked through one way to make a simple accordion list using vanilla JavaScript. Today, I'll explore several ways an accordion list could be tweaked, sometimes for performance, sometimes for semantics, and sometimes just for style. No one solution is ever perfect for everybody, so the aim here is to give you ideas for how to best fit the widget to your needs. And if you don't need an accordion list on your web site, perhaps you can at least walk away having learned a couple questions to ask yourself when adding other fancy widgets to your site. But enough delay...let's get to it!
One-Pass DOM Query
In the original article, the JavaScript used document.querySelectorAll
to find the accordions, and then another accordion.querySelectorAll
to find the click targets within each accordion. This forces the browser to traverse the DOM of the accordions at least twice (more for nested accordions). We can avoid this by querying for only the click targets, and then implicitly finding the accordions from there. Here's the CodePen:
(function () {
var accordions, targets, ancestor, i;
if(!document.querySelectorAll || !document.body.classList || !Array.prototype.indexOf) return;
targets = document.querySelectorAll('.accordion > section > h1');
for (i = 0; i < targets.length; i++) {
ancestor = targets[i].parentNode.parentNode;
if (!ancestor.classList.contains('js')) {
ancestor.accordionTargetBehavior = (function () {
var currentTarget;
return function () {
if (currentTarget)
currentTarget.classList.remove('expanded');
currentTarget = this.parentNode;
currentTarget.classList.add('expanded');
};
})();
}
targets[i].addEventListener('click', ancestor.accordionTargetBehavior, false);
ancestor.classList.add('js');
}
})();
See the Pen Accordion Variation - One DOM Pass by Will Mitchell (@wakamoleguy) on CodePen.
Instead of querying for the accordions, we go straight for the heading elements with document.querySelectorAll('.accordion > section > h1');
and use parentNode
to find the accordion from there. We also take advantage of the .js
class, previously just used to mark accordions as active, to avoid initializing an accordion twice.
Finally, we cache the click handler function on the accordion element itself, where it can contain a reference to the currently expanded element in a closure. Each heading in the accordion uses that same function, and therefore has the ability to update the current target.
I ran this variation through jsPerf to see if the changes actually made a difference, and by golly they did! In both Chrome and Firefox, speeds increased by factors of 2 to 4. You can see the full results and run the tests yourself on the Accordion Selection jsPerf page.
DL-DD-DT - Using Definition Lists for Accordions
When looking for examples of accordions in the wild, I came across this one on Mozilla's Mobile Firefox FAQ page. The list itself uses an unordered list, but each section contains question and answer pairs represented as a definition list. Wouldn't it be cool if the questions themselves expanded out to show the answers?
Unfortunately, as shown by the examples on MDN, definition lists have a man-to-many relationship between terms and definitions. This, along with their flat structure, makes selecting sections of definition lists challenging. With a little help from CSS3 general sibling selectors, we can get past these hurdles.
See the Pen Accordion Variation - dd by Will Mitchell (@wakamoleguy) on CodePen.
The HTML is a simple definition list. Note that Firefox and Chrome both have multiple definitions. The JavaScript is tweaked to select on definition elements rather than sections, but is otherwise the same as the original.
The CSS is where the magic happens. There are three main selectors at work here. .accordion.js > dt.expanded ~ dd
selects all of the definitions following an expanded term. For example, if Chrome is expanded, it will find the Chrome definitions and the Internet Explorer definition. We need to make sure that the IE defintion has CSS of a higher priority applied to it. For that, .accordion.js > dt.expanded ~ dt:not(.expanded) ~ dd
does the trick. After an expanded term, it finds the next unexpanded term, and then selects all of the definitions from then on. Finally, we need a way to select the definitions before the expanded term. .accordion.js > dt:not(.expanded) ~ dd
gets the job done. Note that this is lower priority than the expanded selector, and won't interfere with those styles.
This still doesn't deal with multiple terms that share one definition, although I don't think that would be very difficult. Using JavaScript to mark all those terms as expanded or cleverly applying the adjacent sibling selector (+
) would probably work.
Keyboard Navigation
Our original accordion was not very friendly to keyboard users. It would be nice if we could Tab between accordion headings and expand them by pressing Return. Well, we can with just a little extra markup. By adding 'permalinks' to each heading, we automatically get keyboard access. Pressing enter will invoke the 'click' handler, as well. We just need to be sure to prevent the default behavior of the event to avoid page jumping. Go ahead and click inside the CodePen and use Tab, Shift+Tab, and Return to test it out.
See the Pen Accordion Variation - Keyboard by Will Mitchell (@wakamoleguy) on CodePen.
Multiple Selection
Sometimes it's important to be able to expand multiple sections of the accordion at once. This is an incredibly easy modification...just get rid of tracking the currentTarget:
See the Pen Accordion Variation - Multiple by Will Mitchell (@wakamoleguy) on CodePen.
Accordion? Nah, let's do tabs.
You've spent all this time on an accordion list, and now the designer comes to you saying that accordions are out and tabs are all the rage. Hours of work starting from scratch? Nah! Just a few CSS changes. That's right. All of this uses the same JavaScript and HTML as our original accordion.
See the Pen Accordion Variation - Tabs by Will Mitchell (@wakamoleguy) on CodePen.
Conclusion
As you can see, it's easy to quickly expand the realm of possibilities, even from a base as simple as the vanilla accordion widget. What is important to your website or application? Performance on slower devices? Keyboard controls for accessibility? Most, if not all, of these variations can be mixed and matched, too. And hey, if you are inspired to create your own variations or you found this article helpful or even if you hated it, tweet me about it.
Cheers!