Turning Lists into Trees

by Michal Wojciechowski <odyniec - at - odyniec - dot - net>

In this article, I will present a technique to display a multi-level unordered list in the form of a tree with lines connecting nodes. The final result will look similar to this:

  • Animals
    • Birds
    • Mammals
      • Elephant
      • Mouse
    • Reptiles
  • Plants
    • Flowers
      • Rose
      • Tulip
    • Trees

Note that the tree that we're going to build will be absolutely static – we won't be able to expand/collapse its nodes or anything like that.

Our intention is to avoid cluttering the HTML code with unnecessary tags. Ideally, the code would be as simple as this:

<ul class="tree"> <li>Animals <ul> <li>Birds</li> <li>Mammals <ul> <li>Elephant</li> <li>Mouse</li> </ul> </li> . . . </ul>

So it's just a regular multi-level unordered list, with no additional elements. The only thing that indicates it's a tree is the tree class added to the first-level <ul> element. Without styling, the above code produces a familiar result:

  • Animals
    • Birds
    • Mammals
      • Elephant
      • Mouse
    • Reptiles
  • Plants
    • Flowers
      • Rose
      • Tulip
    • Trees

Bullets Be Gone

The first thing that we need to do is get rid of the ugly bullets. We accomplish this by setting the list-style-type property to none.

ul.tree, ul.tree ul { list-style-type: none; }

This is what we get:

  • Animals
    • Birds
    • Mammals
      • Elephant
      • Mouse
    • Reptiles
  • Plants
    • Flowers
      • Rose
      • Tulip
    • Trees

Drawing Vertical Lines

We need to draw a vertical line along the side of every list, regardless of its nesting level. For that purpose, we'll use a tiny (10 pixels high) image of a vertical line.


vline.png

We put the image in the background of the <ul> element and add the repeat-y value, which causes the image to be repeated along the y-axis, creating a vertical line as high as the list itself. We also adjust the margin and padding properties of the list and list items (<li>) so that the tree is nicely indented.

ul.tree, ul.tree ul { list-style-type: none; background: url(vline.png) repeat-y; margin: 0; padding: 0; } ul.tree ul { margin-left: 10px; } ul.tree li { margin: 0; padding: 0 12px; }

The result:

  • Animals
    • Birds
    • Mammals
      • Elephant
      • Mouse
    • Reptiles
  • Plants
    • Flowers
      • Rose
      • Tulip
    • Trees

Connecting the Nodes

We place an image of a T-shaped line segment in the background of each list item.


node.png

We also set the line-height property to a value equal to the height of the image. For a slightly more attractive look, we set the color of the list item text to #369 and make the font bold.

ul.tree, ul.tree ul { list-style-type: none; background: url(vline.png) repeat-y; margin: 0; padding: 0; } ul.tree ul { margin-left: 10px; } ul.tree li { margin: 0; padding: 0 12px; line-height: 20px; background: url(node.png) no-repeat; color: #369; font-weight: bold; }

Let's see the result:

  • Animals
    • Birds
    • Mammals
      • Elephant
      • Mouse
    • Reptiles
  • Plants
    • Flowers
      • Rose
      • Tulip
    • Trees

Last but not Least

Obviously, the last node in each list requires special treatment. Firstly, it needs to have a different background image – an L-shaped line segment:


lastnode.png

Secondly, the vertical line in the background of the list must end at the last node. We can accomplish this by setting the background color of the last node to white (or whatever other color we're using). The background of the item is placed over the background of the list, so it will cover the last segment of the vertical line.

ul.tree, ul.tree ul { list-style-type: none; background: url(vline.png) repeat-y; margin: 0; padding: 0; } ul.tree ul { margin-left: 10px; } ul.tree li { margin: 0; padding: 0 12px; line-height: 20px; background: url(node.png) no-repeat; color: #369; font-weight: bold; } ul.tree li.last { background: #fff url(lastnode.png) no-repeat; }

The browser does not know which nodes are "last", so it's our responsibility to tell it. We can either explicitly add the last class to the last node of every list:

<ul class="tree"> <li>Animals <ul> <li>Birds</li> <li>Mammals <ul> <li>Elephant</li> <li class="last">Mouse</li> </ul> </li> <li class="last">Reptiles</li> . . . </ul>

...or be lazy and have JavaScript do that for us:

<script type="text/javascript"> window.onload = function () { var tree = document.getElementById("tree"); var lists = [ tree ]; for (var i = 0; i < tree.getElementsByTagName("ul").length; i++) lists[lists.length] = tree.getElementsByTagName("ul")[i]; for (var i = 0; i < lists.length; i++) { var item = lists[i].lastChild; while (!item.tagName || item.tagName.toLowerCase() != "li") item = item.previousSibling; item.className += " last"; } }; </script> . . . <ul class="tree" id="tree"> <li>Animals <ul> <li>Birds</li> <li>Mammals <ul> <li>Elephant</li> <li>Mouse</li> </ul> </li> . . . </ul>

Time to see the final result - voilá!

  • Animals
    • Birds
    • Mammals
      • Elephant
      • Mouse
    • Reptiles
  • Plants
    • Flowers
      • Rose
      • Tulip
    • Trees

You might notice that adding the last class to list items is in conflict with our original idea of not cluttering up the HTML code. Actually, there is an elegant solution to this problem — the :last-child pseudo-class. If we were to use it, we would replace the last class selector with:

ul.tree li:last-child { background: #fff url(lastnode.png) no-repeat; }

Sadly, the pseudo-class is defined in the upcoming CSS3 specification and at the moment few web browsers support it.

Appendix: The jQuery way

JQuery fans, which includes myself, could rewrite the above JavaScript code as:

<script type="text/javascript"> $(document).ready(function () { $('ul.tree li:last-child').addClass('last'); }); </script>