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.

<ol>
    <li class="file"><a href="document.pdf">File 1</a></li>
    <li>
        <label for="subfolder1">Subfolder 1</label>
        <input type="checkbox" id="subfolder1" />
        <ol>
            <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>
        </ol>
    </li>
</ol>

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">
<![endif]-->
<!--[if !IE]>-->
    <link rel="stylesheet" type="text/css" href="_styles.css" media="screen">
<!--<![endif]-->

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.

Short URL: http://cssn.in/ja/026

 

Post filed under: css.

Skip to comment form.

  1. pcdragon says:

    Great work! And thank you for explaining this.

  2. fbender says:

    Is this accessible?
    That’s probably a philosophic question, but I’d like to hear your thoughts.

  3. mazri says:

    nice sharing…tq very much!

  4. Slav says:

    Hi, the menu is great but I have one question.
    I have the menu on the left side in a div container, and my content is on the right in another div as a template for a number of pages. When I expand and click on a link the menu closes to default when opens the page. Is there any way to keep the menu expanded when I click on a link? Thanks

  5. Ingo says:

    Hi, I just wanted to say thanks for this, works like a charme, I love it! Thanks for sharing.

  6. Michael says:

    Checkboxes as expand/collapse buttons? That’s brilliant.

  7. Hi css ninja,

    you’ve done a brilliant work! Thank you for sharing, it has been very useful in a project for me. cheers!

  8. bernhold says:

    This is awesome. How did you find out the checkbox trick? It’s great not having to use JavaScript for this. I don’t like using a bloated JS library just for one collapsible list. One question though, is it allowed to use the input tag outside of a form element? It’s kind of a hack, I suppose, because there’s no actual form.
    At first, I was a little overwhelmed by all the code and explanations. I didn’t really understand what was going on. On my blog, I have posted a simplified barebones example to help understand the underlying technique, maybe it’s useful for other beginners:

    http://bernholdtech.blogspot.de/2013/04/very-simple-pure-css-collapsible-list.html

    Keep up the good work!

  9. jorge says:

    really nice!! just one thing…I haven’t achieved it to works with iE :(
    You said:
    “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”
    I’ve tried with selectivizr, jQuery, NWMatcher and others but nothing…
    I’d appreciate if you go into that ;)
    however…really good work and thanx a lot!

  10. kilimanjaroup says:

    the best css tree I’ve ever seen

  11. Jonas says:

    I got some troubles with IE 8 which is not reading the __style.css properly, also Opera 12.15 is not opening files like File 1 It just dont open excel files, only PDF. waiting answers

  12. jlsakse says:

    Works with IE8 with the help of Selectivizr.

  13. LW says:

    Hi Ryan,
    I am adapting this for a project (I hope that’s ok).

    I just have one question: is there a way of keeping the tree expanded when a subfile is clicked. It currently collapses.

    cheers

    • Ryan Seddon says:

      @LW –

      I’m not sure what your exact problem is but if you’re navigating away from the tree menu to a new page then the state won’t be remembered. You’d have to use JavaScript to alter the checked attribute to keep state.

  14. LW says:

    I have adapted the tree as a container for a large number of links for download (doc, pdf, excel).
    As it stands, if you navigate to say subfile 1 in a subfolder and click on it, the tree automatically collapses requiring the user to navigate again to the same subfolder if they wanted to download subfile 2.

  15. Dave says:

    Great offering, and works across all browsers I have tried too!! Thank you so much and I will be linking to you as soon as my site is up and running. You have really saved me a lot of head scratching.

  16. martin says:

    Great tutorial, :)

    for me it won’t work, because i have another css that already use input check box e.g.
    input[type=”checkbox”],input[type=”radio”]{ … etc…
    Could You please give me a solution?

    Kind Regards

    • Ryan Seddon says:

      @martin I would suggest giving them class names as targeting the input via tag selectors is a slippery slope in a real project.

      Just change any reference to the input to the class e.g.

      .checkbox, .radio {
        ...
      }
      
  17. I love the way this works! Had a question though.

    I want to use larger Icons (and possibly larger text). I’ve added a Background-Size field to the “li label” and the image is larger, but clipped off on the bottom. I changed the size of the “li input + ol” and there’s sufficient space to show the whole icon, but it’s still clipping off the bottom.

    What element do I need to change the height of so it won’t clip it off?

  18. Pieter says:

    Thanks for this excellent piece of CSS!

    One issue: If a have a label with text that is long, so it gets wrapped to another line, then the expand icon also gets to the next line. I would like that it stays on the first line, as you would expect. How would you do that?

  19. Jim says:

    Your Collapsible Tree Menu is perfect for a file download page on a website that I manage. While coding the HTML for 32 nodes / 88 files using Dreamweaver on my local system worked great, placing the same code / css on the site is problematic. The CSS is messing with the site skin CSS and the skin CSS is messing with the Tree Menu CSS.

    The CSS you used is over my head — is there anyway to tweak the style class names to make them “autonomous” to avoid style conflicts?

    Thanks for any suggestions offered!

    • Ryan Seddon says:

      I have plans to clean up my CSS demos and put them on github, one part will be making sure it doesn’t mess up exisiting styles. I don’t have a timeline on this.

  20. Howard says:

    Good work. A very simple solution.

  21. Herval. says:

    Very nice work. Thanks a lot!

  22. Keith Elias says:

    Since I am fairly new to css it took me more than a month to figure out what you had done. I was then able to strip your work down to its minimum although I converted most of the references to class names. The result is here:
    http://jsfiddle.net/Friar_Broccoli/jmw4L/

  23. Keith Elias says:

    @Ryan – Following several hours of reflection/research I found I was (as you can see here: http://jsfiddle.net/Friar_Broccoli/jmw4L/1/ ) able to eliminate my .labelChck class which depended on the for the branch headings. However that caused the area within the box but outside the text area to become unclickable (and confusingly I also lost the cursor indicator) which I don’t want. To your question: It is not immediately obvious to me how I could eliminate the for/id relationship upon which the clickable region seems to depend. Do you have a suggestion?

    Also, given that I don’t yet know JavaScript, is there a CSS way of causing the entire tree to collapse without reloading the menu? (Actually a rather minor problem among the many many I now have.)

    Thanks for your time and your wonderful menu innovation.

  24. Yan Yagunov says:

    IE8 FIX:

    1. Include a JavaScript library, e.g. jQuery from http://jquery.com/ in your head-section.

    Example:

    2. Download and include “Selectivzr” from http://selectivizr.com/ in your head-section.

    Example:

    3. Create and include a stylsheet for IE8.

    Example:

    4. Open your IE8 stylesheet and go to line 58 (“opacity: 0;”). Add “filter: alpha(opacity=0);” in the next line.

    Example:
    li input
    {
    position: absolute;
    left: 0;
    margin-left: 0;
    opacity: 0;
    filter: alpha(opacity=0);
    z-index: 2;
    cursor: pointer;
    height: 1em;
    width: 1em;
    top: 0;
    }

    Thats it. It took me 2 hours to find the problem, till i found out, that there is a problem with “opacity: 0;” which stops Selectivzr from working. Hope it will help you guys :-)

    By the way, great tree navigation, thanks a lot!

    Greetings from Germany,
    Yan

  25. Alejandro Trejo says:

    Hi Ryan, first of all I appreciate the contribution you do with this tree, it is quite simple and practical;’ve adapted this tree so that is generated dynamically according to the results of a large hierarchy of permissions and has been very well … check if there was a similar question but I find that if I did not find the answer … to access a folder tree I have to expand it to view its contents whether subfolders or files, in my case genre subfolders or links that take me to another page, the funny thing is that by giving click the link the tree collapses and is something I do not need you to do because sometimes I just want to see the contents of the link … In a comment that you had done mention about working with javascript to avoid that … I have no idea who seek to prevent or investigate that … Might help me with that.

    I appreciate your support.

  26. Martijn says:

    Is there a non-javascript way to uncheck/collapse an opened subdir when I change to another subdir, so that there is always only 1 subdir opened?

    • Ryan Seddon says:

      @Martijn –

      Yes! If you use radio inputs instead and give them all a common name attribute value then only one radio input can be checked at a time.

  27. Keith Elias says:

    I have put up a (not aesthetic) first working version of an implementation of your code at my website. The “Prong” menu items are associated with pages. In a week or two more code will be added to prevent other menus from closing when a page calls to reload the menu.

  28. Lukas says:

    hi
    great tree…
    i tried to create a tree table with the same effect, but i didnt get it.
    is it possible within a table? do you have examples?

  29. Keith Elias says:

    Concerning the IE8 FIX discussion opened by Yan Yagunov:

    A “simpler” and more generic solution is to add this to the start of your html:

    I just added it to my stripped down version of your ninja CSS menu and it worked perfectly, so hopefully I can just forget about ie8/9 apart from the occasional recheck.

    I found the info here:
    http://stackoverflow.com/questions/11835999/how-can-i-get-css-pseudo-element-checked-to-work-in-ie7-ie8/11842991#11842991
    The developer page is here:
    https://code.google.com/p/ie7-js/

  30. Keith Elias says:

    In the previous code I tried to reference the conditional include for:
    http://ie7-js.googlecode.com/svn/version/2.1(beta4)/IE9.js
    but it was deleted. It’s on the developer’s page.

  31. Eric says:

    Thank you for this excellent example!

    I’m having trouble to adapt this menu to my site:
    In my site, below the tree menu there is some text. When the menu is expanded, the text below is placed correctly, but when the menu is collpased, the text overlaps the label element

    I can workaround this by adding a tag after the tree menu, but i want to know if it’s possible to fix the overlapping adding a css rule. I tried to use margin-bottom but i couldn’t get it to work ;(

    Sorry for my bad english. Thanks

  32. Eric says:

    Sorry, the problem is i was placing the text inside the element. Putting the text outside the ol element works fine. (I also had forgotten input element is a inline element)

  33. Keith Elias says:

    Got your css only menu down to a true minimum. Five css commands (one of which is unnecessary). I also got rid of the class in input, keeping the one in label, but could not get rid of the for/id relationship because I had to contain the input in a tag which then made it impossible for me to use it as a sibling or parent selector. (That may be ambiguous because I don’t have the correct terminology down yet.)

    Here it is:
    http://jsfiddle.net/Friar_Broccoli/6LKc6/

    Thanks again.

  34. amin says:

    how RTL support ?