One of my first tutorials here in Tuts+ back in 2015 covered the creation of an off-canvas sliding menu with jQuery.mmenu. If you check the demo project, you’ll notice that the menu includes multiple levels.
Today, we’ll discuss how to create a three-level deep sliding mobile menu without using any library, just with pure JavaScript. By default, the first one will appear as soon as we open the menu. The other two will come into view with a slide animation upon request.
Ready for another challenge?
Our Multilevel Menu
Check the menu implementation on the pen below:
1. Define the HTML Markup
The markup for our mobile menu will consist of the following elements:
- A
header
with anav
inside it. - A
main
where the main content of our page will live.
Inside the nav
, we’ll put two div
s. The first one will have the header-bar
class, while the second will have the menu-wrapper
one.
The .header-bar
will consist of three elements:
- The toggle menu
- The company logo
- The company's Twitter account
Inside the .menu-wrapper
, we’ll put three div
s with the class of list-wrapper
. We’ll call them panels for simplicity. Things to note:
- In the first panel, we’ll specify the menu structure with parent and child menu items. To do this, we’ll use a typical markup with nested unordered lists.
- The second and third panels will contain a back button and an empty
div
with the class ofsub-menu-wrapper
. More about their job:- The back button will help us go up one level. That said, from level three to level two and from level two to level one.
- The
.sub-menu-wrapper
of the second panel will contain the second-level links. In the same way, the.sub-menu-wrapper
of the third panel will hold the third-level links. We’ll insert these links dynamically through JavaScript.
With all the above in mind, the following markup comes up:
<header class="page-header"> <nav> <div class="header-bar"> <button class="toggle-menu" type="button"> MENU </button> <a href="" class="brand">BRAND</a> <a href="" class="social" target="_blank" title=""> <svg xmlns="https://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> <path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z" /> </svg> </a> </div> <div class="menu-wrapper"> <div class="list-wrapper"> <ul class="menu level-1"> <li> <a href="" class="nested">Categories </a> <ul class="sub-menu level-2"> <li> <a href="" class="nested">Living Room </a> <ul class="sub-menu level-3">...</ul> </li> <li> <a href="">Dining Room</a> </li> ... </ul> </li> <li> <a href="" class="nested">Featured Products</a> <ul class="sub-menu level-2">...</ul> </li> ... </ul> </div> <div class="list-wrapper"> <button type="button" class="back-one-level"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"> <path d="M16.67 0l2.83 2.829-9.339 9.175 9.339 9.167-2.83 2.829-12.17-11.996z" /> </svg> <span>Back</span> </button> <div class="sub-menu-wrapper"></div> </div> <div class="list-wrapper"> <button type="button" class="back-one-level"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"> <path d="M16.67 0l2.83 2.829-9.339 9.175 9.339 9.167-2.83 2.829-12.17-11.996z" /> </svg> <span>Back</span> </button> <div class="sub-menu-wrapper"></div> </div> </div> </nav> </header> <main class="page-main">...</main>
2. Specify the Main Styles
Let’s now concentrate on the header styles for our mobile menu.
For the sake of simplicity, I won’t walk through all the styles, but feel free to look at them by clicking on the CSS tab of the demo project.
Some things to note:
- The header will be a fixed positioned element and have a maximum width of 600px.
- The
.header-bar
will have a fixed height of 60px. - The
.menu-wrapper
will be absolutely positioned and sit underneath the.header-bar
. Furthermore, it will have a height equal to the viewport height minus the.header-bar
’s height. Lastly, it will initially be hidden.
The associated styles:
/*CUSTOM VARIABLES HERE*/ .page-header { position: fixed; top: 0; left: 50%; transform: translateX(-50%); width: 100%; max-width: 600px; margin: 0 auto; color: var(--white); } .page-header .header-bar { display: flex; justify-content: space-between; align-items: center; height: 60px; padding: 0 20px; background: var(--header-bar-bg); } .page-header .menu-wrapper { display: none; position: absolute; top: 60px; left: 0; width: 100%; height: calc(100vh - 60px); overflow: hidden; }
Let’s continue with the panel styles.
- All of them will receive their parent's height and have
overflow-y: auto
. This property ensures that a scrollbar will appear in case there are a lot of menu links inside it. - Especially the second and third panels will be absolutely positioned and out of the screen by default.
The associated styles:
/*CUSTOM VARIABLES HERE*/ .page-header .list-wrapper { height: 100%; padding: 30px 20px; overflow-y: auto; background: var(--menu-bg); } .page-header .list-wrapper:nth-child(2), .page-header .list-wrapper:nth-child(3) { position: absolute; top: 0; left: 0; right: 0; transform: translateX(100%); backface-visibility: hidden; transition: transform 0.5s; }
Next, we'll hide all nested menus from the first and third panels:
.page-header .list-wrapper:nth-child(1) > ul > li > .sub-menu, .page-header .list-wrapper:nth-child(2) .level-3 { display: none; }
Moving on, we’ll set some styles for the mobile menu links, specifically:
- Links that open a nested menu will be underlined.
- To indicate that a link is active or hovered, we'll give it a different color and a character.
The associated styles:
/*CUSTOM VARIABLES HERE*/ .page-header .menu-wrapper a { display: inline-block; position: relative; padding: 5px 0; } .page-header .menu-wrapper a.nested { text-decoration: underline; } .page-header .menu-wrapper a:hover, .page-header .menu-wrapper a.is-active { color: var(--orange); } .page-header .menu-wrapper a:hover::before, .page-header .menu-wrapper a.is-active::before { content: "✦"; position: absolute; top: 50%; right: -20px; transform: translateY(-50%); color: var(--orange); } .page-header .back-one-level { display: flex; align-items: center; margin-bottom: 40px; }
Finally, we’ll specify some straightforward styles for the back button.
Here they are:
.page-header .back-one-level { display: flex; align-items: center; margin-bottom: 40px; } .page-header .back-one-level svg { fill: var(--white); margin-right: 10px; }
3. Add the JavaScript
After setting up the styles, it's time to discuss the actions needed for revealing the nested menu levels with slide animation.
Toggle Menu
Here’s an animated GIF that illustrates the menu’s toggle state:
Each time we click on the toggle button, we’ll perform the following actions:
- Toggle the menu’s visibility via the
is-visible
class. If it’s hidden, it will appear and vice versa. - Check if the menu is closed. If this condition is fulfilled, we’ll do the following:
- Remove the
is-visible
class from the second and third panels if they have it. - Remove the
is-active
class from the active menu links if there are such elements.
- Remove the
Here’s the required JavaScript code:
const pageHeader = document.querySelector(".page-header"); const toggleMenu = pageHeader.querySelector(".toggle-menu"); const menuWrapper = pageHeader.querySelector(".menu-wrapper"); const listWrapper2 = pageHeader.querySelector(".list-wrapper:nth-child(2)"); const listWrapper3 = pageHeader.querySelector(".list-wrapper:nth-child(3)"); const isVisibleClass = "is-visible"; const isActiveClass = "is-active"; toggleMenu.addEventListener("click", function () { // 1 menuWrapper.classList.toggle(isVisibleClass); // 2 if (!this.classList.contains(isVisibleClass)) { // 1 listWrapper2.classList.remove(isVisibleClass); listWrapper3.classList.remove(isVisibleClass); // 2 const menuLinks = menuWrapper.querySelectorAll(".is-active"); for (const menuLink of menuLinks) { menuLink.classList.remove(isActiveClass); } } });
And the relevant styles:
.page-header .menu-wrapper.is-visible { display: block; } .page-header .list-wrapper:nth-child(2), .page-header .list-wrapper:nth-child(3) { transition: transform 0.5s; } .page-header .list-wrapper:nth-child(2).is-visible, .page-header .list-wrapper:nth-child(3).is-visible { transform: none; }
Open Level Two
Here’s an animated GIF that illustrates how the second level menus will appear:
Each time we click on a menu link of the first level (the visible ones), we’ll check to see if it has a nested menu as a sibling. If this condition is fulfilled, we’ll perform the following actions:
- Prevent its default action.
- Assign the
is-active
class to it. - Create a deep copy of its sibling.
- Append this newly created node into the
.sub-menu-wrapper
of the second panel. - Reveal the second panel with slide animation.
Here’s the required JavaScript code:
... for (const level1Link of level1Links) { level1Link.addEventListener("click", function (e) { const siblingList = level1Link.nextElementSibling; if (siblingList) { // 1 e.preventDefault(); // 2 this.classList.add(isActiveClass); // 3 const cloneSiblingList = siblingList.cloneNode(true); // 4 subMenuWrapper2.innerHTML = ""; subMenuWrapper2.append(cloneSiblingList); // 5 listWrapper2.classList.add(isVisibleClass); } }); }
And the associated styles for the animation:
.page-header .list-wrapper:nth-child(2) { transition: transform 0.5s; } .page-header .list-wrapper:nth-child(2).is-visible { transform: none; }
Open Level Three
Here’s an animated GIF that illustrates how the third level menus will appear:
Remember that by default, there isn’t any content inside the .sub-menu-wrapper
of the second panel. In the previous section, we described how it receives content by performing a deep copy.
Here’s an example of the generated markup:
Next, we have to perform some actions as soon as someone clicks on a menu link of the second level. However, the tricky thing is that these links are dynamically generated and aren’t part of the initial DOM. That said, the click
event won’t work for these elements :(
Happily, the solution is simple enough. Thanks to the event delegation, we’ll attach the click
event to the parent panel which is part of the DOM. Then, via the target
property of that event, we’ll check the elements on which the event occurred to ensure that these are links with a sub-menu sibling.
Assuming these are the target elements, for each one of them, we’ll do the following things (similar to the previous section):
- Prevent its default action.
- Assign the
is-active
class to it. - Create a (deep) copy of its sibling. Note that there won’t be any change if we do a deep copy or not, as our menu contains three levels.
- Append this newly created node into the
.sub-menu-wrapper
of the third panel. - Reveal the third panel with slide animation.
Here’s the required JavaScript code:
... listWrapper2.addEventListener("click", function (e) { const target = e.target; if (target.tagName.toLowerCase() === "a" && target.nextElementSibling) { const siblingList = target.nextElementSibling; // 1 e.preventDefault(); // 2 target.classList.add(isActiveClass); // 3 const cloneSiblingList = siblingList.cloneNode(false); // 4 subMenuWrapper3.innerHTML = ""; subMenuWrapper3.append(cloneSiblingList); // 5 listWrapper3.classList.add(isVisibleClass); } });
And the relevant styles for the animation:
.page-header .list-wrapper:nth-child(3) { transition: transform 0.5s; } .page-header .list-wrapper:nth-child(3).is-visible { transform: none; }
Go Back One Level
Here’s an animated GIF that illustrates how the back buttons will work:
Each time we click on a back button, we’ll perform the following actions.
- Find the parent panel of the target back button and remove its
is-visible
class. - Remove the
is-active
class from the active link of the previous sibling of the parent panel.
Here’s the required JavaScript code:
... for (const backOneLevelBtn of backOneLevelBtns) { backOneLevelBtn.addEventListener("click", function () { // 1 const parent = this.closest(".list-wrapper"); parent.classList.remove(isVisibleClass); // 2 parent.previousElementSibling .querySelector(".is-active") .classList.remove(isActiveClass); }); }
And again, there are some accompanying CSS styles for this action.
Conclusion
In this tutorial, we created a multi-level mobile menu by taking advantage of some common CSS styles and JavaScript DOM’s API. Most importantly, our initial markup contains simple nested lists for menu creation. That means we can transpile it into a dynamic one and take advantage of a CMS’s features like WordPress with just a few modifications.
I’m sure there will be other solutions for building such menus, perhaps more effective or accessible than this one. Our approach takes advantage heavily of the cloneNode()
method that creates duplicate nodes, so it should be used with caution, especially if our sub-menus contain IDs.
Here’s a reminder of our today’s exercise:
If you liked this exercise and it saved you some time for building such a solution from scratch, don’t forget to give it some ❤️. Plus, if you’d like to see another version or an extension of this menu, let us know!
Last but not least, if you find the idea of building custom WordPress menus challenging, I’ve good news for you: some of my upcoming tutorials will target this topic. Stay tuned!
As always, thanks a lot for reading!
No comments:
Post a Comment