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.

View live demo Download the source files

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 class="document.pdf">File 1</a></li>
    <li>
        <label for="subfolder1">Subfolder 1</label>
        <input type="checkbox" id="subfolder1" />
        <ol>
            <li class="file"><a class="">File 2</a></li>
            <li class="file"><a class="">File 2</a></li>
            <li class="file"><a class="">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" class="_styles.css" media="screen">
<![endif]-->
<!--[if !IE]>-->
    <link rel="stylesheet" type="text/css" class="_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: cssn.in/ja/026

 

Post filed under: css.

Skip to comment form.

  1. spacer Akhil says:
    February 4, 2012 at 3:38 pm

    thank you so much…… the exact and compact code which i needd…….

  2. spacer Prodyot says:
    February 18, 2012 at 8:16 am

    Great tutorial.
    Thanks.

  3. spacer altp says:
    February 28, 2012 at 10:52 pm

    Can anyone help me out with IE8?

  4. spacer Robert says:
    March 1, 2012 at 11:07 pm

    Hi,
    This menu is such a treat! The best part of this menu system is that it works even when Javascript is disabled thus relieving us from worrying about JS enabled/disabled status. I can confirm that this works in Google Chrome v 16.0.912.63

    Thanks for the wonderful resource.

  5. spacer Milton Keynes Web Design says:
    March 6, 2012 at 7:28 pm

    Another great example of CSS work.

  6. spacer Cali says:
    March 9, 2012 at 10:13 am

    I love the concept here, I have a question though. can you use the same concept on tables, trs, and td’s?

    I am interested in using this to hide large amounts of text, if you can imagine with me… I will be applying this in documenting release notes.

    I see where another user used this with text, but I’m not sure what positive if the elements were switched.

    Thanks!

  7. spacer Irene says:
    March 13, 2012 at 1:54 am

    Great article! I have a question. I have a problem with the position of the tree inside a gadget in the blogger. Can I move the position of the tree more to the left or I have to move part by part?
    Thanks.

  8. spacer Cassandra says:
    June 30, 2012 at 8:42 am

    I am attempting to understand this css style. Can anyone help me know how to add to this style the ability to enter paragraphs under a given folder with normal bullets in front of some of it?

    Folder 1
    Subfolder1
    Paragraphs (no graphics in front)
    More paragraphs
    * list item 1
    * list item 2
    More paragraphs
    LINK File
    Subfolder 2
    Link File
    Link File

    Any help would be appreciated!
    Cassandra

  9. spacer matt says:
    July 18, 2012 at 1:27 am

    If you are having trouble with folder one just take out the everything in folder 1. Then change folder 2 to 1 and so on.

  10. spacer Jonas says:
    August 15, 2012 at 10:58 pm

    Question:
    How can Collapse/Expand All with Javascript?

    Very good, I use in my web app. Congratulations.
    Thanks

  11. spacer Ryan Seddon says:
    August 17, 2012 at 2:24 pm

    @Jonas –

    You could iterate over all the checkboxes and set the checked property to true and the rever for collapsing them.

  12. spacer Michael Schoonover says:
    September 15, 2012 at 8:08 am

    Wow, fantastic work. Very ingenious. Thanks for sharing the knowledge.

  13. spacer pcdragon says:
    October 4, 2012 at 6:38 am

    Great work! And thank you for explaining this.

  14. spacer fbender says:
    October 22, 2012 at 2:29 am

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

  15. spacer mazri says:
    November 2, 2012 at 1:13 pm

    nice sharing…tq very much!

  16. spacer Slav says:
    November 18, 2012 at 5:52 am

    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

  17. spacer Ingo says:
    November 29, 2012 at 5:02 pm

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

  18. spacer Michael says:
    January 7, 2013 at 4:22 pm

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

  19. spacer Khayrattee Wasseem says:
    January 17, 2013 at 9:04 pm

    Hi css ninja,

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

« Older Comments

Leave a comment