A simple nested tree menu with vanilla js and css

Share on:

Nested tree menu gif

While building a dashboard layout, I recently had a requirement for a nested tree menu. It would preferably have to support any arbitrary level of nesting. There were some packages available already like JSTree - but they were built to support a lot of extra functionalities like changing the menu theme, adding branches dynamically etc which made them bigger than I needed them to be. I didn’t need any of those features and hence decided to build it myself. This short post is intended to document the same

Designing the html structure

The default choice for any menu type structure is to go with unordered lists. We can do the same here. We just need to make sure that the structure supports deep nesting and sub-headings for sub-menus. At the bare minimum it would look like this


    <ul class="listree">
            <li>
                <div class="listree-submenu-heading">Personal Settings</div>
                <ul class="listree-submenu-items">
                <li><a href="">Password Settings</a></li>
                <li><a href="">Viewing Preferences</a></li>
                </ul>
            </li>
            <li>
                <div class="listree-submenu-heading">Org Settings</div>
                <ul class="listree-submenu-items">
                <li><a href="">Teams</a></li>
                <li><a href="">Billing</a></li>
                </ul>
            </li>
     </ul>

Making it interactive using css and js

Here we implement the styling and scripts that would make this a proper nested menu

Step 1: Remove the default ul styling which we will do by targetting the class name

        ul.listree {
            list-style: none;
        }
        ul.listree-submenu-items {
            list-style: none;
        }

Step 2: Add a click handler which expands and collapses the submenu on clicking the heading

We do this by targetting all the elements with the submenu heading’s class name and then setting it’s sibling element’s (which would be the submenu ul) display to none (to hide it by default). Then we add a click handler on the submenu heading div which toggles the sibling submenu ul’s display property between none and block

        function listree() {
            const subMenuHeadings = document.getElementsByClassName("listree-submenu-heading");
            Array.from(subMenuHeadings).forEach(function(subMenuHeading){
                subMenuHeading.nextElementSibling.style.display = "none";
                subMenuHeading.addEventListener('click', function(event){
                    event.preventDefault();
                    const subMenuList = event.target.nextElementSibling;
                    if(subMenuList.style.display=="none"){
                        subMenuList.style.display = "block";
                    }
                    else {
                        subMenuList.style.display = "none";
                    }
                    event.stopPropagation();
                });
            });
        }

Step 3: Add + and - markers next to the headings to denote its state

The menu headings should show a plus next to them when they are in a collapsed state indicating that they can be expanded and a minus next to them when they are expanded.

To do this we first add two classes - collapsed and expanded on the submenu heading which add the characters + and -

        div.listree-submenu-heading.collapsed:before {
            content: "+";
            margin-right: 4px;
        }
        div.listree-submenu-heading.expanded:before {
            content: "-";
            margin-right: 4px;
        }

And then we add the logic to add and remove the classes to the event handler


        function listree() {
            const subMenuHeadings = document.getElementsByClassName("listree-submenu-heading");
            Array.from(subMenuHeadings).forEach(function(subMenuHeading){
                subMenuHeading.classList.add("collapsed");
                subMenuHeading.nextElementSibling.style.display = "none";
                subMenuHeading.addEventListener('click', function(event){
                    event.preventDefault();
                    const subMenuList = event.target.nextElementSibling;
                    if(subMenuList.style.display=="none"){
                        subMenuHeading.classList.remove("collapsed");
                        subMenuHeading.classList.add("expanded");
                        subMenuList.style.display = "block";
                    }
                    else {
                        subMenuHeading.classList.remove("expanded");
                        subMenuHeading.classList.add("collapsed");
                        subMenuList.style.display = "none";
                    }
                    event.stopPropagation();
                });
            });
        }

Step 4: Add a submenu boundary when it is expanded

A final touch would be to add a dotted boundary on the left when a submenu is expanded - which lets the user see clearly which items are in the same level of nesting. To do this we add the following css


        ul.listree-submenu-items {
            list-style: none;
            border-left: 1px dashed black;
            white-space: nowrap;
            margin-right: 4px;
            padding-left: 20px;
        }

Since we have already added the styling and logic for hiding and showing this class, this boundary also will be hidden and shown along with it.

And that finishes our work. Combining it all together, here is a sample html with the styling and the script which achieves the nested menu functionality

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style type="text/css">
        .listree-submenu-heading {
            cursor: pointer;
        }
        ul.listree {
            list-style: none;
        }
        ul.listree-submenu-items {
            list-style: none;
            border-left: 1px dashed black;
            white-space: nowrap;
            margin-right: 4px;
            padding-left: 20px;
        }
        div.listree-submenu-heading.collapsed:before {
            content: "+";
            margin-right: 4px;
        }
        div.listree-submenu-heading.expanded:before {
            content: "-";
            margin-right: 4px;
        }
    </style>
    <title>Easy Tree</title>
  </head>
  <body>
    <ul class="listree">
        <li>
            <div class="listree-submenu-heading">Metrics</div>
            <ul class="listree-submenu-items">
            <li>
                <div class="listree-submenu-heading">Daily Metrics</div>
                <ul class="listree-submenu-items">
                <li>
                    <div class="listree-submenu-heading">Daily Order Metrics</div>
                    <ul class="listree-submenu-items">
                    <li>
                        <div class="listree-submenu-heading">Categorywise Daily order metrics</div>
                        <ul class="listree-submenu-items">
                        <li>
                            <a href="">Categorywise daily order count</a>
                        </li>
                        <li>
                            <a href="">Categorywise daily bookings</a>
                        </li>
                        </ul>
                    </li>
                    <li>
                        <div class="listree-submenu-heading">Storewise Daily order metrics</div>
                        <ul class="listree-submenu-items">
                        <li>
                            <a href="">Storewise daily order count</a>
                        </li>
                        <li>
                            <a href="">Storewise daily bookings</a>
                        </li>
                        </ul>
                    </li>
                    </ul>
                </li>
                <li>
                    <div class="listree-submenu-heading">Daily Invoice Metrics</div>
                    <ul class="listree-submenu-items">
                    <li>
                        <div class="listree-submenu-heading">Categorywise Daily invoice metrics</div>
                        <ul class="listree-submenu-items">
                        <li>
                            <a href="">Categorywise daily invoice count</a>
                        </li>
                        <li>
                            <a href="">Categorywise daily revenue</a>
                        </li>
                        </ul>
                    </li>
                    <li>
                        <div class="listree-submenu-heading">Storewise Daily invoice metrics</div>
                        <ul class="listree-submenu-items">
                        <li>
                            <a href="">Storewise daily invoice count</a>
                        </li>
                        <li>
                            <a href="">Storewise daily revenue</a>
                        </li>
                        </ul>
                    </li>
                    </ul>
                </li>
                </ul>
            </li>
            <li>
                <div class="listree-submenu-heading">Monthly Metrics</div>
                <ul class="listree-submenu-items">
                <li>
                    <div class="listree-submenu-heading">Monthly Order Metrics</div>
                    <ul class="listree-submenu-items">
                    <li>
                        <div class="listree-submenu-heading">Categorywise Monthly order metrics</div>
                        <ul class="listree-submenu-items">
                        <li>
                            <a href="">Categorywise monthly order count</a>
                        </li>
                        <li>
                            <a href="">Categorywise monthly bookings</a>
                        </li>
                        </ul>
                    </li>
                    <li>
                        <div class="listree-submenu-heading">Storewise Monthly order metrics</div>
                        <ul class="listree-submenu-items">
                        <li>
                            <a href="">Storewise monthly order count</a>
                        </li>
                        <li>
                            <a href="">Storewise monthly bookings</a>
                        </li>
                        </ul>
                    </li>
                    </ul>
                </li>
                <li>
                    <div class="listree-submenu-heading">Monthly Invoice Metrics</div>
                    <ul class="listree-submenu-items">
                    <li>
                        <div class="listree-submenu-heading">Categorywise Monthly invoice metrics</div>
                        <ul class="listree-submenu-items">
                        <li>
                            <a href="">Categorywise monthly invoice count</a>
                        </li>
                        <li>
                            <a href="">Categorywise monthly revenue</a>
                        </li>
                        </ul>
                    </li>
                    <li>
                        <div class="listree-submenu-heading">Storewise Monthly invoice metrics</div>
                        <ul class="listree-submenu-items">
                        <li>
                            <a href="">Storewise monthly invoice count</a>
                        </li>
                        <li>
                            <a href="">Storewise monthly revenue</a>
                        </li>
                        </ul>
                    </li>
                    </ul>
                </li>
                </ul>
            </li>
            </ul>
        </li>
        <li>
            <div class="listree-submenu-heading">Settings</div>
            <ul class="listree-submenu-items">
            <li>
                <div class="listree-submenu-heading">Personal Settings</div>
                <ul class="listree-submenu-items">
                <li><a href="">Password Settings</a></li>
                <li><a href="">Viewing Preferences</a></li>
                </ul>
            </li>
            <li>
                <div class="listree-submenu-heading">Org Settings</div>
                <ul class="listree-submenu-items">
                <li><a href="">Teams</a></li>
                <li><a href="">Billing</a></li>
                </ul>
            </li>
            </ul>
        </li>
    </ul>
    <script type="text/javascript">
        function listree() {
            const subMenuHeadings = document.getElementsByClassName("listree-submenu-heading");
            Array.from(subMenuHeadings).forEach(function(subMenuHeading){
                subMenuHeading.classList.add("collapsed");
                subMenuHeading.nextElementSibling.style.display = "none";
                subMenuHeading.addEventListener('click', function(event){
                    event.preventDefault();
                    const subMenuList = event.target.nextElementSibling;
                    if(subMenuList.style.display=="none"){
                        subMenuHeading.classList.remove("collapsed");
                        subMenuHeading.classList.add("expanded");
                        subMenuList.style.display = "block";
                    }
                    else {
                        subMenuHeading.classList.remove("expanded");
                        subMenuHeading.classList.add("collapsed");
                        subMenuList.style.display = "none";
                    }
                    event.stopPropagation();
                });
            });
        }
        listree();
   </script>
  </body>
</html>

Now that it is working as expected, we will see how we can convert this into a reusable npm package in the next post

comments powered by Disqus