Pure CSS collapsible tree menu

The classic tree view, we all know it, it’s used everywhere and it definitely can be useful in the right context. I’ve seen various examples about doing it with CSS and they’ve all required JavaScript. Not content with any of those solutions I investigated doing it with pure CSS, I got a good head start from my Custom Radio and Checkbox inputs article. From there I’ve come up with a solution that works pretty well.


Another demo, another bug

Everything I seem to investigate lately seems to present itself with an annoying bug/feature in various browsers. Last time it was the inconsistency between browsers and generated content on form elements. This time it is WebKit not being able to apply styles using the checked pseudo-class in conjunction with a general sibling combinator (E ~ F) or chained adjacent sibling combinator (E + F + F). Making it very hard, and probably the reason I haven’t seen a CSS solution that works in WebKit browsers. I did come across this demo but due to the bug mentioned above doesn’t work in WebKit browsers.

So I soldiered on and came up with a pretty decent attempt, and remember folks I’m not a designer so be kinder this time with design critiques all I’m doing is showing you how to do the technique ;). With that out of the way let’s dig into the inner workings and road blocks I faced.

General sibling combinators are flaky

The CSS3 selector module has a very useful addition to compliment the CSS2.1 adjacent sibling combinator. Unlike the adjacent selector the general selector gives us some flexibility in that it will match a sibling that isn’t immediately preceded by our first element.

Great, I have 3 elements an <input>, <label> & <ol> the general sibling combinator is the perfect tool to do things like input:checked ~ ol. Check Firefox, awesome works, Opera too! Woo! Surely WebKit will have this…nope nothing. Let me try input ~ ol, yep works across the board, *face palm*.

So I dug into WebKits bug tracker and came out with this bug which has been around since 2007. Stating that general sibling combinators in combination with dynamic CSS, ala :checked, won’t reflect changes. Nor will it do chained adjacent combinator which was going to be my next solution.

However doing :checked with a single adjacent sibling combinator works fine in post 2008 WebKit browsers. So using this information I went about and built a working demo that has good browser support.

The demo is built using an ordered list (ol) nested with further ordered lists to naturally represent a basic folder structure.

    <li class="file"><a href="document.pdf">File 1</a></li>
        <label for="subfolder1">Subfolder 1</label>
        <input type="checkbox" id="subfolder1" />
            <li class="file"><a href="">File 2</a></li>
            <li class="file"><a href="">File 2</a></li>
            <li class="file"><a href="">File 2</a></li>

As you can see, in order to get around the general combinator issue in WebKit based browsers I have switched the label to come first then the input so the “folders” could be expanded/collapsed by checking/unchecking the checkbox.

li input {
    position: absolute;
    left: 0;
    margin-left: 0;
    opacity: 0;
    z-index: 2;
    cursor: pointer;
    height: 1em;
    width: 1em;
    top: 0;
li label {
    background: url(folder-horizontal.png) 15px 1px no-repeat;
    cursor: pointer;
    display: block;
    padding-left: 37px;

To sit the input and label in the right visual order I absolutely positioned the input and applied a left padding to the label to push it out. That way changing the styles of the child ol, when the input is checked, can be done using the adjacent sibling combinator. I’ve also set the cursor to pointer when hovered over the input or label to visual show they’re clickable.

li input + ol {
    background: url(toggle-small-expand.png) 40px 0 no-repeat;
    margin: -0.938em 0 0 -44px; /* 15px */
    display: block;
    height: 1em;

Unlike my custom radio and checkbox article where I added the background image on the label, this time I had to do some trickery and apply it to the direct sibling ol of an input. Applying a sprite image to the ol wouldn’t be possible in this situation due to it being applied to the ol which would make it difficult to effectively hide other images within the sprite image.

To position the ol correctly I use a negative margin to pull it into the right location so it will sit next to the label and underneath the invisible checkbox.

li input + ol > li {
    display: none;
    margin-left: -14px !important;
    padding-left: 1px;

To hide the sub folders so they don’t appear when the parent folder is collapsed I target the child list items and set them to, a zero height and hide any overflowAndy pointed out that I could just use display: none over height, this also stops the keyboard navigation from tabbing into non expanded items as they’re now hidden.

li label {
    background: url(folder.png) 15px 1px no-repeat;
li.file a {
    background: url(document.png) 0 -1px no-repeat;
    color: #fff;
    padding-left: 21px;
    text-decoration: none;
    display: block;

To differentiate between folders and files I applied a background image to either the label or to an anchor within a list item for files.

li {
    position: relative;
    margin-left: -15px;
    list-style: none;
li.file {
    margin-left: -1px !important;

To pull out the folder list items I apply a larger negative margin so the folder will line up with any of the file icons, and for file based list items I reset the left margin so they sit flush.

Change icon based on file extension

With some CSS3 attribute selectors we can determine an anchor links file format and change the icon accordingly.

li.file a[href $= '.pdf']     { background-position: -16px -1px; }
li.file a[href $= '.html']    { background-position: -32px -1px; }
li.file a[href $= '.css']     { background-position: -48px -1px; }
li.file a[href $= '.js']      { background-position: -64px -1px; }

Using the $= CSS attribute selector allows us to check the end of an attribute exactly ends in .pdf, .html etc.

If for some reason you attribute doesn’t end with your file extension, your anchor may have a query string on the end. We can still match file types.

li.file a[href *= '.pdf']   { background-position: -16px -1px; }
li.file a[href *= '.html']  { background-position: -32px -1px; }

The *= will match a substring that contains .pdf or .html anywhere within the attribute and if a href has a query string we can still match our file extension without issue. This does have the slight disadvantage that it will match it anywhere e.g. if you have a file called file.html.pdf it will match both file types and the one with the higher CSS specificity will be applied or incase of the example above their CSS specificity is the same so the html background will be applied.

Checkbox attributes

In the demo by default the first folder is open, this is done by adding the checked attribute to the checkbox which will trigger our styles thanks to the checked pseudo-class and reveal its sub files and folders.

We can also add the disabled attribute to the checkbox to stop a user from opening a folder as the input can neither be checked or unchecked.

Lastly using a combination of both disabled and checked will allow us to reveal the sub files and folders but not allow the user to close the top level folder.

Browser support

Based on testing this will work in any CSS3 selector supporting browser. The following have been tested and known to work

  • Firefox 1+
  • Opera 9.6+
  • Safari 4+
  • iPhone/iPod Safari
  • Chrome 1+
  • Android
  • IE9+

This could very well work in IE8 but would require some JavaScript to get IE8 to interpret the checked pseudo-class, which I won’t be going into.

Right now I use conditional comments to hide the stylesheet from all versions of IE and another conditional comment to load the stylesheet for IE9 and greater.

<!--[if gte IE 9 ]>
    <link rel="stylesheet" type="text/css" href="_styles.css" media="screen">
<!--[if !IE]>-->
    <link rel="stylesheet" type="text/css" href="_styles.css" media="screen">

Highly scalable

This technique will cater for a large amount of sub folders and files. It’s governed by your screen real estate and even then it’ll apply scroll bars to the document when the tree structure gets too long or wide.

Any questions/comments/suggestions leave a comment.

[link href=”http://cssn.in/ja/026″]