Collapsible Nested Lists in Vanilla JavaScript

Display mode

Back to Articles

We all know that a tree, like the one seen in Windows' File Explorer, is nothing more than a nested list. But is it possible to code a tree up in HTML/CSS as a nested list?

Let's start off with the list. This is a snippet of a standard file tree, organised in "folders".

<ul>
 <li>
  Graphics
  <ul>
   <li><a href='gpu.h'>gpu.h: Function prototypes</a></li>
   <li><a href='gpu.c'>gpu.c: Graphic output implementation</a></li>
   <li>
    Debug Output
    <ul>
     <li><a href='dbgout.h'>dbgout.h: Output prototypes</a></li>
     <li><a href='dbgout.c'>dbgout.c: Output fixed-width font drawing</a></li>
     <li><a href='font5x7.h'>font5x7.h: Fixed-width font definitions</a></li>
    </ul>
   </li>
  </ul>
 </li>
</ul>

And this is what we get from that code.

Having a tree means that each branching node can expand or collapse, to show or hide the elements of the tree within it. The showing and the hiding isn't so difficult; the display property in CSS allows us to do this pretty quickly, if we define two classes:

ul.hide { display: none; }
ul.show { display: block; }

Of course, it's not quite that simple. You can't change the state of a UL very easily (clicking on it won't do); but you can change the state of an LI. So we move the two classes to the enclosing LI:

li.hide ul { display: none; }
li.show ul { display: block; }

So, we have the CSS for hiding the tree. But how do we switch states? How can we show and hide the nodes at will? That's where the DOM comes in. If we put the description of the tree item ("Debug Output" for example") in an active element (DIV or A maybe), we can attach DOM events to it.

I've decided not to use A, because an anchor requires a href, and using a link of # will clutter up your browser's History facility. So, let's use a DIV.

What do we want to happen when we click the DIV? Basically, just flip the state of the parent LI, such that the ULs underneath are visible.

<ul>
 <li class='hide'>
  <div onclick='toggle(this.parentNode)'>Graphics</div>
  <ul>
   <li><a href='gpu.h'>gpu.h: Function prototypes</a></li>
   <li><a href='gpu.c'>gpu.c: Graphic output implementation</a></li>
   <li class='hide'>
    <div onclick='toggle(this.parentNode)'>Debug Output</div>
    <ul>
     <li><a href='dbgout.h'>dbgout.h: Output prototypes</a></li>
     <li><a href='dbgout.c'>dbgout.c: Output fixed-width font drawing</a></li>
     <li><a href='font5x7.h'>font5x7.h: Fixed-width font definitions</a></li>
    </ul>
   </li>
  </ul>
 </li>
</ul>

So when you click the "Graphics" DIV, toggle() runs and flips the top LI from hide to show. And of course, if you click it again, it flips back to hide. We'll need some JavaScript to do this; fortunately, JS gives us the ternary operator, where we can select two options based on a condition.

function toggle(x)
{
  x.className = (x.className=='show') ? 'hide' : 'show';
}

What this means is: "If the className is show, set it to hide, otherwise [ie. if it's not show] set it to show". Since there're only two possibilities for the class name, you can see that this toggles between the two.

Just before we get to an actual working example, you should remember that you'll have to define CSS for each level of menu that we go down, since the properties won't inherit between ULs if there's an LI in the way (which there always is).

So now, we can put it all together, and come up with a simple tree that is collapsible/expandable with a bit of DOM fiddling.

Here, I've just added some styling to the text DIV, which can change along with the parent LI state just as the UL does. Again, the inheritance of properties will be lost between levels, so just put in an extra line for each level down.

Now we have just one problem. As you can see, the page loads with the Graphics item collapsed. What if you don't have JS running? Click on the DIV and nothing happens; you can't get to the list underneath! Obviously a problem. The way to alleviate this is to have everything expanded by default instead of collapsed; if you need to, use an onload tree collapse so that the tree will collapse if you run JS, and stay expanded if you don't.

function treeCollapse(){
  var list = document.getElementById('yourtree').getElementsByTagName('li');
  for(var i=0;i<list.length;i++) list[i].className = 'hide';
}

This'll just get a list of all the LIs in the tree, and set them to class hide.

So, that's how to make a nested list into an expandable tree.