We had an interesting challenge today, where we needed to create a collapsible, responsive menu and sub-menu, using pure JavaScript.
Normally this functionality would be part of a frontend framework, but the client’s site doesn’t use any so, rather than using one solely for this functionality, we decided a clean and efficient way would be to write it in pure JavaScript.
The Components
Here are the fruits of the couple of hours this little beaut took to code up. The HMTL is explicitly for a responsive nav menu, whereas the CSS and JavaScript have been written to support toggling any elements.
HTML
The HTML’s for a menu and buttons to toggler its and its submenu’s visibilty. This is pretty self-explanitory, so I haven’t bothered littering it with unnecesary comments. All we have is a header
element that contains some togglers and a nav
element with items and a submenu.
<header role="banner">
<div class="d-md-none">
<button class="btn-toggler btn-menu" data-toggle="visiblilty" data-target="#nav-primary">☰</button>
<button class="btn-toggler btn-close" data-toggle="visiblilty" data-target="#nav-primary">×</button>
</div>
<nav id="nav-primary" role="navigation" class="nav-menu hide">
<ul>
<li><a href="#">home</a></li>
<li><a href="#">about</a></li>
<li><button class='btn-link btn-submenu' data-toggle="visiblilty" data-target="#nav-submenu">sub-menu <span>▼</span></button>
<ul id="nav-submenu" role="navigation" class="-hide">
<li><a href="#">sub 1</a></li>
<li><a href="#">sub 2</a></li>
<li><a href="#">sub 3</a></li>
</ul>
</li>
<li><a href="#">contact</a></li>
</ul>
</nav>
</header>
CSS
The CSS is ‘mobile-first’, meaning the default styles are intended for smaller devices, with larger devices catered for using @media
queries. The default layout of the nav is stacked, but hidden and toggled with a toggler button. As is the submenu. For larger devices the nav menu is always displayed in a horizontal layout.
/** Base */body {
font-size: 16px;
font-family: arial, sans-serif;
}
/** Buttons */.btn-toggler,
.btn-link {
outline-width: 0 ;
background-color: transparent;
font-family: arial, sans-serif;
}
.btn-link {
font-size: 16px;
border-width: 0;
padding: 0;
text-decoration: underline;
cursor: pointer;
}
.btn-toggler {
font-size: 1.5rem;
line-height: 1;
text-transform: uppercase;
padding: 0.25rem 0.5rem;
min-width: 50px;
color: tomato;
border: 2px solid currentColor;
}
.btn-toggler:not(:last-of-type) {
margin-right: 0.5rem;
}
.btn-menu[aria-expanded='true'],
.btn-close[aria-expanded='false'] {
display: none;
}
.btn-submenu span {
font-size: 0.5em;
line-height: 1.75;
}
/** Nav menus */.nav-menu ul {
display: flex;
flex-direction: column;
list-style: none;
padding: 0;
}
.nav-menu a,
.nav-menu .btn-link {
display: flex;
box-sizing: border-box;
padding: 0.5rem;
width: 100%;
text-decoration: none;
text-transform: uppercase;
color: tomato;
}
.nav-menu a:hover,
.nav-menu .btn-link:hover {
background-color: rgba(0, 0, 0, 0.1);
}
/** Hide element */.hide {
display: none ;
}
/** Media breakpoints */@media (min-width: 768px) {
.d-md-none {
display: none;
}
/** Show nav menu */ .nav-menu {
display: flex ;
}
/** Display menu items horizonatally */ .nav-menu > ul {
flex-direction: row;
}
}
JavaScript
Toggling the visisbilty of elements is done using JavaScript, by toggling state classes and aria-expanded
attributes.
var toggler = document.querySelectorAll("[data-toggle='visiblilty']"),
targetShown,
target,
newTarget = true;
// loop through all togglers
for (var i=0; i<=toggler.length; i++) {
// for efficiency, only process a toggler's target if it's different from the previous target
if (document.querySelector(toggler[i].dataset.target) === target) {
newTarget = false;
}
// set the target element and its visibility class
if (newTarget) {
// get the target element
target = document.querySelector(toggler[i].dataset.target);
// ascertain the target's current visibilty
targetShown = !target.classList.contains('hide');
}
// set the toggler's aria-expanded state based on the target's visibilty class
toggler[i].setAttribute('aria-expanded', targetShown);
// listen for toggler click events
toggler[i].onclick = function() {
// get the target element
target = document.querySelector(this.dataset.target);
// toggle the target's visibilty class and toggler's aria-expanded state
if (target.classList.contains('hide')) {
target.classList.replace("hide", "show");
targetShown = true;
} else {
// precautionarily set the 'show' class (incase it doesn't exist)
target.classList.add("show");
target.classList.replace("show", "hide");
targetShown = false;
}
// update the aria-expanded state for this target's togglers
for (var j = 0; j <=toggler.length; j++) {
if (toggler[j].dataset.target === this.dataset.target) {
toggler[j].setAttribute('aria-expanded', targetShown);
}
}
};
}
Conclusion
Using frontend frameworks to handle the nuts-n-bolts of HTML, CSS and JS normally address most common layout and functional aspects of a website, but there are often times when specific requirements need to be written to achieve the desired outcome. This example shows that it’s often not too difficult or time-consuming to implement them (if you know what you’re doing).
We love these challenges, so if you come across any that you’d like a hand with please get in touch and we’ll gladly take on the challenge.