Multi-Level Menu

A simple multi-level menu with delayed item animations and an optional breadcrumb navigation and back button.

Today’s Blueprint is a simple menu with multiple levels. The idea is to animate each menu item once a level is changed. The animation starts with the item clicked and the delays are propagated through the neighbors. The animation delays follow the same logic for the incoming items of the new level of the multi-level menu. As optional elements we have a breadcrumb navigation and a back button (not shown in our demo). Deeper levels are referenced with a data attribute. We have added some example media queries for a mobile menu version with a menu toggle. We’ve also provided a simple callback example.

The icons used in the demo are from the Organic Food icon set by Wojciech Zasina and the Feather icon set by Cole Bemis.

Please note that we are using a couple of modern CSS properties, so only modern browsers are supported.

Browser Support:
  • ChromeSupported
  • FirefoxSupported
  • Internet ExplorerSupported from version 10+
  • SafariSupported
  • OperaSupported

The HTML

<!-- Main container -->
<div class="container">
	<!-- Menu toggle for mobile version -->
	<button class="action action--open" aria-label="Open Menu"><span class="icon icon--menu"></span></button>
	<!-- Menu -->
	<nav id="ml-menu" class="menu">
		<!-- Close button for mobile version -->
		<button class="action action--close" aria-label="Close Menu"><span class="icon icon--cross"></span></button>
		<div class="menu__wrap">
			<ul data-menu="main" class="menu__level">
				<li class="menu__item"><a class="menu__link" data-submenu="submenu-1" href="#">Vegetables</a></li>
				<li class="menu__item"><a class="menu__link" data-submenu="submenu-2" href="#">Fruits</a></li>
				<li class="menu__item"><a class="menu__link" data-submenu="submenu-3" href="#">Grains</a></li>
				<li class="menu__item"><a class="menu__link" data-submenu="submenu-4" href="#">Mylk & Drinks</a></li>
			</ul>
			<!-- Submenu 1 -->
			<ul data-menu="submenu-1" class="menu__level">
				<li class="menu__item"><a class="menu__link" href="#">Stalk Vegetables</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Roots & Seeds</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Cabbages</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Salad Greens</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Mushrooms</a></li>
				<li class="menu__item"><a class="menu__link" data-submenu="submenu-1-1" href="#">Sale %</a></li>
			</ul>
			<!-- Submenu 1-1 -->
			<ul data-menu="submenu-1-1" class="menu__level">
				<li class="menu__item"><a class="menu__link" href="#">Fair Trade Roots</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Dried Veggies</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Our Brand</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Homemade</a></li>
			</ul>
			<!-- Submenu 2 -->
			<ul data-menu="submenu-2" class="menu__level">
				<li class="menu__item"><a class="menu__link" href="#">Citrus Fruits</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Berries</a></li>
				<li class="menu__item"><a class="menu__link" data-submenu="submenu-2-1" href="#">Special Selection</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Tropical Fruits</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Melons</a></li>
			</ul>
			<!-- Submenu 2-1 -->
			<ul data-menu="submenu-2-1" class="menu__level">
				<li class="menu__item"><a class="menu__link" href="#">Exotic Mixes</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Wild Pick</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Vitamin Boosters</a></li>
			</ul>
			<!-- Submenu 3 -->
			<ul data-menu="submenu-3" class="menu__level">
				<li class="menu__item"><a class="menu__link" href="#">Buckwheat</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Millet</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Quinoa</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Wild Rice</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Durum Wheat</a></li>
				<li class="menu__item"><a class="menu__link" data-submenu="submenu-3-1" href="#">Promo Packs</a></li>
			</ul>
			<!-- Submenu 3-1 -->
			<ul data-menu="submenu-3-1" class="menu__level">
				<li class="menu__item"><a class="menu__link" href="#">Starter Kit</a></li>
				<li class="menu__item"><a class="menu__link" href="#">The Essential 8</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Bolivian Secrets</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Flour Packs</a></li>
			</ul>
			<!-- Submenu 4 -->
			<ul data-menu="submenu-4" class="menu__level">
				<li class="menu__item"><a class="menu__link" href="#">Grain Mylks</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Seed Mylks</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Nut Mylks</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Nutri Drinks</a></li>
				<li class="menu__item"><a class="menu__link" data-submenu="submenu-4-1" href="#">Selection</a></li>
			</ul>
			<!-- Submenu 4-1 -->
			<ul data-menu="submenu-4-1" class="menu__level">
				<li class="menu__item"><a class="menu__link" href="#">Nut Mylk Packs</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Amino Acid Heaven</a></li>
				<li class="menu__item"><a class="menu__link" href="#">Allergy Free</a></li>
			</ul>
		</div>
	</nav>
	<div class="content">
		<p class="info">Please choose a category</p>
		<!-- Ajax loaded content here -->
	</div>
</div>
<!-- /view -->
<script src="js/classie.js"></script>
<script src="js/dummydata.js"></script>
<script src="js/main.js"></script>
<script>
(function() {
	var menuEl = document.getElementById('ml-menu'),
		mlmenu = new MLMenu(menuEl, {
			// breadcrumbsCtrl : true, // show breadcrumbs
			// initialBreadcrumb : 'all', // initial breadcrumb text
			backCtrl : false, // show back button
			// itemsDelayInterval : 60, // delay between each menu item sliding animation
			onItemClick: loadDummyData // callback: item that doesn´t have a submenu gets clicked - onItemClick([event], [inner HTML of the clicked item])
		});

	// mobile menu toggle
	var openMenuCtrl = document.querySelector('.action--open'),
		closeMenuCtrl = document.querySelector('.action--close');

	openMenuCtrl.addEventListener('click', openMenu);
	closeMenuCtrl.addEventListener('click', closeMenu);

	function openMenu() {
		classie.add(menuEl, 'menu--open');
	}

	function closeMenu() {
		classie.remove(menuEl, 'menu--open');
	}

	// simulate grid content loading
	var gridWrapper = document.querySelector('.content');

	function loadDummyData(ev, itemName) {
		ev.preventDefault();

		closeMenu();
		gridWrapper.innerHTML = '';
		classie.add(gridWrapper, 'content--loading');
		setTimeout(function() {
			classie.remove(gridWrapper, 'content--loading');
			gridWrapper.innerHTML = '<ul class="products">' + dummyData[itemName] + '<ul>';
		}, 700);
	}
})();
</script>

The CSS

/* Icons (made with Icomoon.io) */
/* Feather Icons by Cole Bemis */
@font-face {
	font-family: 'feather';
	font-weight: normal;
	font-style: normal;
	src: url('../fonts/feather/feather.eot?1gafuo');
	src: url('../fonts/feather/feather.eot?1gafuo#iefix') format('embedded-opentype'), url('../fonts/feather/feather.woff2?1gafuo') format('woff2'), url('../fonts/feather/feather.ttf?1gafuo') format('truetype'), url('../fonts/feather/feather.woff?1gafuo') format('woff'), url('../fonts/feather/feather.svg?1gafuo#feather') format('svg');
}

.icon {
	font-family: 'feather';
	font-weight: normal;
	font-style: normal;
	font-variant: normal;
	line-height: 1;
	text-transform: none;
	/* Better Font Rendering =========== */
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
	speak: none;
}

.icon--arrow-left:before {
	content: 'e901';
}

.icon--menu:before {
	content: 'e903';
}

.icon--cross:before {
	content: 'e117';
} 

/* Menu styles */

.menu {
	position: fixed;
	top: 120px;
	left: 0;
	width: 300px;
	height: calc(100vh - 120px);
	background: #1c1d22;
}

.menu__wrap {
	position: absolute;
	top: 3.5em;
	bottom: 0;
	overflow: hidden;
	width: 100%;
}

.menu__level {
	position: absolute;
	top: 0;
	left: 0;
	visibility: hidden;
	overflow: hidden;
	overflow-y: scroll;
	width: calc(100% + 50px);
	height: 100%;
	margin: 0;
	padding: 0;
	list-style-type: none;
}

.menu__level--current {
	visibility: visible;
}

.menu__item {
	display: block;
	width: calc(100% - 50px);
}

.menu__link {
	font-weight: 600;
	position: relative;
	display: block;
	padding: 1em 2.5em 1em 1.5em;
	color: #bdbdbd;
	-webkit-transition: color 0.1s;
	transition: color 0.1s;
}

.menu__link[data-submenu]::after {
	content: 'e904';
	font-family: 'feather';
	position: absolute;
	right: 0;
	padding: 0.25em 1.25em;
	color: #2a2b30;
}

.menu__link:hover,
.menu__link[data-submenu]:hover::after {
	color: #5c5edc;
}

.menu__link--current::before {
	content: '0B7';
	font-size: 1.5em;
	line-height: 0;
	position: absolute;
	top: 50%;
	left: 0.5em;
	height: 4px;
	color: #5c5edc;
}

[class^='animate-'],
[class*=' animate-'] {
	visibility: visible;
}

.animate-outToRight .menu__item {
	-webkit-animation: outToRight 0.6s both cubic-bezier(0.7, 0, 0.3, 1);
	animation: outToRight 0.6s both cubic-bezier(0.7, 0, 0.3, 1);
}

@-webkit-keyframes outToRight {
	to {
		opacity: 0;
		-webkit-transform: translate3d(100%, 0, 0);
		transform: translate3d(100%, 0, 0);
	}
}

@keyframes outToRight {
	to {
		opacity: 0;
		-webkit-transform: translate3d(100%, 0, 0);
		transform: translate3d(100%, 0, 0);
	}
}

.animate-outToLeft .menu__item {
	-webkit-animation: outToLeft 0.6s both cubic-bezier(0.7, 0, 0.3, 1);
	animation: outToLeft 0.6s both cubic-bezier(0.7, 0, 0.3, 1);
}

@-webkit-keyframes outToLeft {
	to {
		opacity: 0;
		-webkit-transform: translate3d(-100%, 0, 0);
		transform: translate3d(-100%, 0, 0);
	}
}

@keyframes outToLeft {
	to {
		opacity: 0;
		-webkit-transform: translate3d(-100%, 0, 0);
		transform: translate3d(-100%, 0, 0);
	}
}

.animate-inFromLeft .menu__item {
	-webkit-animation: inFromLeft 0.6s both cubic-bezier(0.7, 0, 0.3, 1);
	animation: inFromLeft 0.6s both cubic-bezier(0.7, 0, 0.3, 1);
}

@-webkit-keyframes inFromLeft {
	from {
		opacity: 0;
		-webkit-transform: translate3d(-100%, 0, 0);
		transform: translate3d(-100%, 0, 0);
	}
	to {
		opacity: 1;
		-webkit-transform: translate3d(0, 0, 0);
		transform: translate3d(0, 0, 0);
	}
}

@keyframes inFromLeft {
	from {
		opacity: 0;
		-webkit-transform: translate3d(-100%, 0, 0);
		transform: translate3d(-100%, 0, 0);
	}
	to {
		opacity: 1;
		-webkit-transform: translate3d(0, 0, 0);
		transform: translate3d(0, 0, 0);
	}
}

.animate-inFromRight .menu__item {
	-webkit-animation: inFromRight 0.6s both cubic-bezier(0.7, 0, 0.3, 1);
	animation: inFromRight 0.6s both cubic-bezier(0.7, 0, 0.3, 1);
}

@-webkit-keyframes inFromRight {
	from {
		opacity: 0;
		-webkit-transform: translate3d(100%, 0, 0);
		transform: translate3d(100%, 0, 0);
	}
	to {
		opacity: 1;
		-webkit-transform: translate3d(0, 0, 0);
		transform: translate3d(0, 0, 0);
	}
}

@keyframes inFromRight {
	from {
		opacity: 0;
		-webkit-transform: translate3d(100%, 0, 0);
		transform: translate3d(100%, 0, 0);
	}
	to {
		opacity: 1;
		-webkit-transform: translate3d(0, 0, 0);
		transform: translate3d(0, 0, 0);
	}
}

.menu__breadcrumbs {
	font-size: 0.65em;
	line-height: 1;
	position: relative;
	padding: 2.5em 3.75em 1.5em 2.5em;
}

.menu__breadcrumbs a {
	font-weight: bold;
	display: inline-block;
	cursor: pointer;
	vertical-align: middle;
	letter-spacing: 1px;
	text-transform: uppercase;
	color: #5c5edc;
}

.menu__breadcrumbs a:last-child {
	pointer-events: none;
}

.menu__breadcrumbs a:hover {
	color: #8182e0;
}

.menu__breadcrumbs a:not(:last-child)::after {
	content: 'e902';
	font-family: 'feather';
	display: inline-block;
	padding: 0 0.5em;
	color: #33353e;
}

.menu__breadcrumbs a:not(:last-child):hover::after {
	color: #33353e;
}

.menu__back {
	font-size: 1.05em;
	position: absolute;
	z-index: 100;
	top: 0;
	right: 2.25em;
	margin: 0;
	padding: 1.365em 0.65em 0 0;
	cursor: pointer;
	color: #2a2b30;
	border: none;
	background: none;
}

.menu__back--hidden {
	pointer-events: none;
	opacity: 0;
}

.menu__back:hover,
.menu__back:focus {
	color: #fff;
	outline: none;
}

/* Open and close buttons */

.action {
	position: absolute;
	display: block;
	margin: 0;
	padding: 0;
	cursor: pointer;
	border: none;
	background: none;
}

.action:focus {
	outline: none;
}

.action--open {
	font-size: 1.5em;
	top: 1em;
	left: 1em;
	display: none;
	color: #fff;
	position: fixed;
	z-index: 1000;
}

.action--close {
	font-size: 1.1em;
	top: 1.25em;
	right: 1em;
	display: none;
	color: #45464e;
}

/* Example media query */
@media screen and (max-width: 40em) {
	.action--open,
	.action--close {
		display: block;
	}
	.menu {
		z-index: 1000;
		top: 0;
		width: 100%;
		height: 100vh;
		-webkit-transform: translate3d(-100%, 0, 0);
		transform: translate3d(-100%, 0, 0);
		-webkit-transition: -webkit-transform 0.3s;
		transition: transform 0.3s;
	}
	.menu--open {
		-webkit-transform: translate3d(0, 0, 0);
		transform: translate3d(0, 0, 0);
	}
}

The JavaScript

/**
 * main.js
 * http://www.codrops.com
 *
 * Licensed under the MIT license.
 * http://www.opensource.org/licenses/mit-license.php
 * 
 * Copyright 2015, Codrops
 * http://www.codrops.com
 */
;(function(window) {

	'use strict';

	var support = { animations : Modernizr.cssanimations },
		animEndEventNames = { 'WebkitAnimation' : 'webkitAnimationEnd', 'OAnimation' : 'oAnimationEnd', 'msAnimation' : 'MSAnimationEnd', 'animation' : 'animationend' },
		animEndEventName = animEndEventNames[ Modernizr.prefixed( 'animation' ) ],
		onEndAnimation = function( el, callback ) {
			var onEndCallbackFn = function( ev ) {
				if( support.animations ) {
					if( ev.target != this ) return;
					this.removeEventListener( animEndEventName, onEndCallbackFn );
				}
				if( callback && typeof callback === 'function' ) { callback.call(); }
			};
			if( support.animations ) {
				el.addEventListener( animEndEventName, onEndCallbackFn );
			}
			else {
				onEndCallbackFn();
			}
		};

	function extend( a, b ) {
		for( var key in b ) { 
			if( b.hasOwnProperty( key ) ) {
				a[key] = b[key];
			}
		}
		return a;
	}

	function MLMenu(el, options) {
		this.el = el;
		this.options = extend( {}, this.options );
		extend( this.options, options );
		
		// the menus (<ul>´s)
		this.menus = [].slice.call(this.el.querySelectorAll('.menu__level'));
		// index of current menu
		this.current = 0;

		this._init();
	}

	MLMenu.prototype.options = {
		// show breadcrumbs
		breadcrumbsCtrl : true,
		// initial breadcrumb text
		initialBreadcrumb : 'all',
		// show back button
		backCtrl : true,
		// delay between each menu item sliding animation
		itemsDelayInterval : 60,
		// direction 
		direction : 'r2l',
		// callback: item that doesn´t have a submenu gets clicked
		// onItemClick([event], [inner HTML of the clicked item])
		onItemClick : function(ev, itemName) { return false; }
	};

	MLMenu.prototype._init = function() {
		// iterate the existing menus and create an array of menus, more specifically an array of objects where each one holds the info of each menu element and its menu items
		this.menusArr = [];
		var self = this;
		this.menus.forEach(function(menuEl, pos) {
			var menu = {menuEl : menuEl, menuItems : [].slice.call(menuEl.querySelectorAll('.menu__item'))};
			self.menusArr.push(menu);

			// set current menu class
			if( pos === self.current ) {
				classie.add(menuEl, 'menu__level--current');
			}
		});

		// create back button
		if( this.options.backCtrl ) {
			this.backCtrl = document.createElement('button');
			this.backCtrl.className = 'menu__back menu__back--hidden';
			this.backCtrl.setAttribute('aria-label', 'Go back');
			this.backCtrl.innerHTML = '<span class="icon icon--arrow-left"></span>';
			this.el.insertBefore(this.backCtrl, this.el.firstChild);
		}
		
		
		// create breadcrumbs
		if( self.options.breadcrumbsCtrl ) {
			this.breadcrumbsCtrl = document.createElement('nav');
			this.breadcrumbsCtrl.className = 'menu__breadcrumbs';
			this.el.insertBefore(this.breadcrumbsCtrl, this.el.firstChild);
			// add initial breadcrumb
			this._addBreadcrumb(0);
		}

		// event binding
		this._initEvents();
	};

	MLMenu.prototype._initEvents = function() {
		var self = this;

		for(var i = 0, len = this.menusArr.length; i < len; ++i) {
			this.menusArr[i].menuItems.forEach(function(item, pos) {
				item.querySelector('a').addEventListener('click', function(ev) { 
					var submenu = ev.target.getAttribute('data-submenu'),
						itemName = ev.target.innerHTML,
						subMenuEl = self.el.querySelector('ul[data-menu="' + submenu + '"]');

					// check if there's a sub menu for this item
					if( submenu && subMenuEl ) {
						ev.preventDefault();
						// open it
						self._openSubMenu(subMenuEl, pos, itemName);
					}
					else {
						// add class current
						var currentlink = self.el.querySelector('.menu__link--current');
						if( currentlink ) {
							classie.remove(self.el.querySelector('.menu__link--current'), 'menu__link--current');
						}
						classie.add(ev.target, 'menu__link--current');
						
						// callback
						self.options.onItemClick(ev, itemName);
					}
				});
			});
		}
		
		// back navigation
		if( this.options.backCtrl ) {
			this.backCtrl.addEventListener('click', function() {
				self._back();
			});
		}
	};

	MLMenu.prototype._openSubMenu = function(subMenuEl, clickPosition, subMenuName) {
		if( this.isAnimating ) {
			return false;
		}
		this.isAnimating = true;
		
		// save "parent" menu index for back navigation
		this.menusArr[this.menus.indexOf(subMenuEl)].backIdx = this.current;
		// save "parent" menu´s name
		this.menusArr[this.menus.indexOf(subMenuEl)].name = subMenuName;
		// current menu slides out
		this._menuOut(clickPosition);
		// next menu (submenu) slides in
		this._menuIn(subMenuEl, clickPosition);
	};

	MLMenu.prototype._back = function() {
		if( this.isAnimating ) {
			return false;
		}
		this.isAnimating = true;

		// current menu slides out
		this._menuOut();
		// next menu (previous menu) slides in
		var backMenu = this.menusArr[this.menusArr[this.current].backIdx].menuEl;
		this._menuIn(backMenu);

		// remove last breadcrumb
		if( this.options.breadcrumbsCtrl ) {
			this.breadcrumbsCtrl.removeChild(this.breadcrumbsCtrl.lastElementChild);
		}
	};

	MLMenu.prototype._menuOut = function(clickPosition) {
		// the current menu
		var self = this,
			currentMenu = this.menusArr[this.current].menuEl,
			isBackNavigation = typeof clickPosition == 'undefined' ? true : false;

		// slide out current menu items - first, set the delays for the items
		this.menusArr[this.current].menuItems.forEach(function(item, pos) {
			item.style.WebkitAnimationDelay = item.style.animationDelay = isBackNavigation ? parseInt(pos * self.options.itemsDelayInterval) + 'ms' : parseInt(Math.abs(clickPosition - pos) * self.options.itemsDelayInterval) + 'ms';
		});
		// animation class
		if( this.options.direction === 'r2l' ) {
			classie.add(currentMenu, !isBackNavigation ? 'animate-outToLeft' : 'animate-outToRight');
		}
		else {
			classie.add(currentMenu, isBackNavigation ? 'animate-outToLeft' : 'animate-outToRight');	
		}
	};

	MLMenu.prototype._menuIn = function(nextMenuEl, clickPosition) {
		var self = this,
			// the current menu
			currentMenu = this.menusArr[this.current].menuEl,
			isBackNavigation = typeof clickPosition == 'undefined' ? true : false,
			// index of the nextMenuEl
			nextMenuIdx = this.menus.indexOf(nextMenuEl),

			nextMenuItems = this.menusArr[nextMenuIdx].menuItems,
			nextMenuItemsTotal = nextMenuItems.length;

		// slide in next menu items - first, set the delays for the items
		nextMenuItems.forEach(function(item, pos) {
			item.style.WebkitAnimationDelay = item.style.animationDelay = isBackNavigation ? parseInt(pos * self.options.itemsDelayInterval) + 'ms' : parseInt(Math.abs(clickPosition - pos) * self.options.itemsDelayInterval) + 'ms';

			// we need to reset the classes once the last item animates in
			// the "last item" is the farthest from the clicked item
			// let's calculate the index of the farthest item
			var farthestIdx = clickPosition <= nextMenuItemsTotal/2 || isBackNavigation ? nextMenuItemsTotal - 1 : 0;

			if( pos === farthestIdx ) {
				onEndAnimation(item, function() {
					// reset classes
					if( self.options.direction === 'r2l' ) {
						classie.remove(currentMenu, !isBackNavigation ? 'animate-outToLeft' : 'animate-outToRight');
						classie.remove(nextMenuEl, !isBackNavigation ? 'animate-inFromRight' : 'animate-inFromLeft');
					}
					else {
						classie.remove(currentMenu, isBackNavigation ? 'animate-outToLeft' : 'animate-outToRight');
						classie.remove(nextMenuEl, isBackNavigation ? 'animate-inFromRight' : 'animate-inFromLeft');
					}
					classie.remove(currentMenu, 'menu__level--current');
					classie.add(nextMenuEl, 'menu__level--current');

					//reset current
					self.current = nextMenuIdx;

					// control back button and breadcrumbs navigation elements
					if( !isBackNavigation ) {
						// show back button
						if( self.options.backCtrl ) {
							classie.remove(self.backCtrl, 'menu__back--hidden');
						}
						
						// add breadcrumb
						self._addBreadcrumb(nextMenuIdx);
					}
					else if( self.current === 0 && self.options.backCtrl ) {
						// hide back button
						classie.add(self.backCtrl, 'menu__back--hidden');
					}

					// we can navigate again..
					self.isAnimating = false;
				});
			}
		});	
		
		// animation class
		if( this.options.direction === 'r2l' ) {
			classie.add(nextMenuEl, !isBackNavigation ? 'animate-inFromRight' : 'animate-inFromLeft');
		}
		else {
			classie.add(nextMenuEl, isBackNavigation ? 'animate-inFromRight' : 'animate-inFromLeft');
		}
	};

	MLMenu.prototype._addBreadcrumb = function(idx) {
		if( !this.options.breadcrumbsCtrl ) {
			return false;
		}

		var bc = document.createElement('a');
		bc.innerHTML = idx ? this.menusArr[idx].name : this.options.initialBreadcrumb;
		this.breadcrumbsCtrl.appendChild(bc);

		var self = this;
		bc.addEventListener('click', function(ev) {
			ev.preventDefault();

			// do nothing if this breadcrumb is the last one in the list of breadcrumbs
			if( !bc.nextSibling || self.isAnimating ) {
				return false;
			}
			self.isAnimating = true;
			
			// current menu slides out
			self._menuOut();
			// next menu slides in
			var nextMenu = self.menusArr[idx].menuEl;
			self._menuIn(nextMenu);

			// remove breadcrumbs that are ahead
			var siblingNode;
			while (siblingNode = bc.nextSibling) {
				self.breadcrumbsCtrl.removeChild(siblingNode);
			}
		});
	};

	window.MLMenu = MLMenu;

})(window);

Tagged with:

Manoela Ilic

Manoela is the main tinkerer at Codrops. With a background in coding and passion for all things design, she creates web experiments and keeps frontend professionals informed about the latest trends.

Stay up to date with the latest web design and development news and relevant updates from Codrops.

Feedback 70

Comments are closed.
  1. Very nice demo!

    Animation direction seems to be inverted though. Typically if you go into submenu, content is moved to the left side, not right. At least iOS follows this logic.

    • Superb demo! So slick! The sliding of content in from the left is better. From the right doesn’t feel as established.

    • Thanks for pointing that out 🙂 We’ve added a new option that allows to choose the direction. The default one opens to the left. Cheers, ML

  2. Beautiful!

    When the view port height enables scrolling on the menu, how did you get it to scroll without showing a scroll bar?

    Great stuff 🙂

  3. Very Nice … I Have Followed Your Articles For A Few Years Now And You Have Improved Web/APP Nav In My Opinion ,. Im Using A Few Of Your Concepts In My Project , Expand Overlay’s , Multi-Level Nav And Some Of Your Css Layouts .. Thanks For Your Beautiful Work.

  4. Can you integrate this with history push-state / pop-state?
    Would be great if it supported browser back / forward buttons!

    Also, would be nice if a page was loaded even when drilling down.

    For example if you click on Fruits, on the right all fruits would load.
    And then when you click on berries, it would only load the berries.
    Essentially asking if its possible to load a page on each click.

  5. Of course we all love your work, But there’s a big issue in this project, The sub menus are out of its (li)s!! Why is that? I try to use it in my project but i notice that i must change my framework so i used your old project Responsive Multi-Level Menu .. BUT .. I really want to try this awesome menu.

    Thanks for work .. Keep going…

    Regards, Hady

  6. How can I implement a anchor link at the last submenu? I have exactly this:

    Menu > submenu > submenu 2 > link to other page

    But when I click on the “link to other page” it just close the menu and do nothing.

  7. How can i load gallery of pictures of my own instead of dummy data? Do i insert my code on the dummydata.js instead of loading the icons in each line? Or is it somewhere else?

  8. When using this on an iPad (and presumably mobile device) when scrolling up and down the page it does not continue scrolling with momentum after you release your finger. How do you stop the code from preventing this?

    • I’m also interested in similar thing. So i want to get data for all submenu dynamically (by ajax). So i have main categories but when i click on one ajax is sending get request to database and append submenu to html as , bu problem is that i don’t know how to refresh function of jquery to get new data. Can someone help me?

    • Not sure if this was answered.

      In the page that you have the menu there is code at the bottom remove or comment out the following:

      //onItemClick: loadDummyData // callback: item that doesn´t have a submenu gets clicked – onItemClick([event], [inner HTML of the clicked item])

      // simulate grid content loading
      /* var gridWrapper = document.querySelector(‘.content’);

      function loadDummyData(ev, itemName) {
      ev.preventDefault();

      closeMenu();
      gridWrapper.innerHTML = ”;
      classie.add(gridWrapper, ‘content–loading’);
      setTimeout(function() {
      classie.remove(gridWrapper, ‘content–loading’);
      gridWrapper.innerHTML = ” + dummyData[itemName] + ”;
      }, 700);
      }*/

      Once these are commented out you can add URL to the links in the menu above.

  9. Hey, very nice work!

    But I really want to know, how to load the innerHTML content out of an external file. Is it possible with $(gridwrapper).load?
    I want to set the dummyImage like
    var dummyImage { "Menu1": Menu1.html, }
    I tried a lot, but I don’t get it.

    • If you need to retrieve the data from server you can replace the ‘setTimeout function, for example (i’ve not tested it):

      /* setTimeout(function() { classie.remove(gridWrapper, 'content--loading'); gridWrapper.innerHTML = '<ul class="products">' + dummyData[itemName] + '</ul>'; }, 700); */ //this can be *.php | *.ashx | etc. var elemestsHandlerURL = "HandlersDirectory/ElementsHandler.php"; //parameters you send to the server via POST: var postParameters = { ElementType : dummyData[itemName] }; //clear container and shoe progressbar gridWrapper.innerHTML = ''; classie.add(gridWrapper, 'content--loading'); //ajax: $.post(elemestsHandlerURL, postParameters, function(elementsWrapperObj){ if(!elementsWrapperObj.Success){ alert('REQUEST FAILED!'); return; } var $ul = $('<ul />').addClass('products'); for(var i=0; i<elementsWrapperObj.Elements.length;i++){ var item = elementsWrapperObj.Elements[i]; var $li = $('<li />').addClass('product'); var $clickableItem = $('<div />').addClass('foodicon').addClass(elementsWrapperObj.CssClass).text(item.Name); $clickableItem.click(function(){ var $currentSelectedItem = $(this); alert($currentSelectedItem.html()); }); $clickableItem.appendTo($li); $li.appendTo($ul); } classie.remove(gridWrapper, 'content--loading'); $ul.appendTo(gridWrapper); /* * elementsWrapperObj must have the following structure: { "Success": "true", //or false "CssClass": "foodicon--broccoli", //according to the type of the elements "Elements": [{"Name": "Poduct 1"}, {"Name": "Poduct 2"}] } */ });//post

  10. Good Job, I really like the breadcrumbs, What about making it into a mobile site so that it disappears to a menu button like page-stack-navigation 🙂

  11. Nice effect and easy to drill-down!
    I think the breadcrumb links might get to small. Less optimal for narrow screens.
    I would prefer a “back button”… maybe with parent menuitem title.

  12. How can I open this menu in specific state, after reload. I don’t wanna use JS to load content to the content div. I wanna use a little bit complicated structure.

    • Heyi had the same problem. In the main.js you can add this:

      When you add ‘data-submenu-open=”1″‘ to your opened .menu__item of the main-menu and add following code to the main.js on line 138, then it will trigger your prefered submenu.

      if (item.dataset.submenuOpen == 1) {
      item.querySelector(‘a’).click()
      }

    • Don’t know if you still need this but I found a cool solution.

      add Jquery simulate to your site (https://github.com/jquery/jquery-simulate/blob/master/jquery.simulate.js)

      Then give each link that you want to click to a unique “id” name.

      E.g.
      Level 1 menu has id=”menu1″
      Level 1-1 menu, item 1 has id=”menu11″

      then at the bottom use of the page add the following script

      $( “#menu1” ).simulate( “click” );
      setTimeout(function(){ $( “#menu11” ).simulate( “click” ); }, 1000);

      What will happen is the moment the page loads the #menu1 will get a simulated click (the nav will animate to sub menu)
      If you need to click to another submenu use a timer after 1 second the menu will get another simulated click to the sub-sub menu.

      If you want you can go directly to #menu11 but the back arrow will take you directly to the top menu. Having the timer will all normal navigation moving up the menu tree.

      Also having the menu slide to different levels will help visitor see that they are not in the top menu level.

    • Damium,

      Tried your solution but can’t get it to work… could you please copy/paste the section of main.js so I can see how it’s coded? Thank you!

  13. (function($, MLMenu){
    $.fn.MLMenu = function(option)
    {
    return this.each(function() {
    MLMenu(this, option);
    });
    };
    })(window.jQuery, window.MLMenu);

  14. Hi,

    Just wondering. I need to load data from ajax therefore need to pass category ids instead of names because there are subcategories with similar names.

    I added the following to line 117 of main.js

    itemId = ev.target.getAttribute('data-id'),

    and then edited line 69:

    onItemClick : function(ev, itemName, itemId) { return false; }

    then added data-id to menu items:

    <a class="menu__link" href="#" data-id="10">Pre-Workout</a>

    and finally here is my callback function:

    function loadData(ev, itemName,itemId)

    When I run the the code, only the value of itemId is not the id passed but it’s = itemName

  15. Hey, thanks for this awesome blueprint. I have an urgent request. I tried to use this blueprint as a non-ajax Main-Navigation for a CMS. After figuring out the current page and the related submenu I set the according index to this.current in the JS-File. The according submenu is opening when a subpage is loaded, which is fine, but the breadcrumbs and back controls are not behaving accordingly, because it thinks the opened submenu is the new root-level. I already opened an issue on GitHub, but no answer so far. Any ideas on how to set/open a submenu on initialization?

  16. This is beautifully done. The only addition I would make, is:

    .menu__breadcrumbs a:last-child {
    color: #CCC
    }

    Something to make it clear that the last/active child menu item in the breadcrumb is not clickable. It already has the mouse pointer removed, but possibly make it a little more obvious.

  17. Is it possible to sync/create the breadcrumbs for a subpage when it is not loaded through ajax?

  18. This is amazing! If you made this into a WordPress Plugin I’d me more than happy to buy it. Any chance of that happening?

  19. Hi, Great plugin but i have no idea how to remember apoened panel, so if page is reloaded, plugin shows main panel. Is this possible ?
    Thank you.

  20. Has anyone used this as a wordpress menu? I’m trying to do this but because of the wp_nav_menu function, I’ve to include the submenus as under the main menu which breaks the animation (makes it clunky). How would I fix this?

  21. How can I get the links to go to html files? I want to integrate with WordPress but when I try to use external links as suggested by @Raymond above, the menu disappears. Anyone had any success with this?

    • In order to work the external link (the last one with href),just delete the “onItemClick” so: onItemClick: loadDummyData – also the loadDummyData(){…} function.

      P.S – on the demo link “https://tympanus.net/Blueprints/MultiLevelMenu/” the menu works on my iphone 4s, but when I download the source, I click on the menu and it doesn’t open. This is very strange, is there any different script called online (which I dont think so). Any idea ???

  22. Hey, thanks very much. for this awesome blueprint. I have an urgent request. I tried to use this blueprint as a non-ajax Main-Navigation for a CMS. After figuring out the current page and the related submenu I set the according index to this.current in the JS-File. The according submenu is opening when a subpage is loaded, which is fine, but the breadcrumbs and back controls are not behaving accordingly, because it thinks the opened submenu is the new root-level. I already opened an issue on GitHub, but no answer so far. Any ideas on how to set/open a submenu on initialization?

  23. This plugin is awesome, but I have problem with crossbrowser test. It says multybrowser support, how I can run on IE10+ and Microsoft Edge. Menu is blank even if I already allow block content.

  24. Do you really need all that JavaScript ??

    What if I just wanted a menu with different levels could I just use JQuery(Hide/Show/AddClass) etc etc I’m not bothered about the history displaying links either ?

  25. Maybe you mentioned …,
    How can I change the icon to display div content in html?
    …I’m sorry, I’m not professional
    Thank you
    Michal

  26. This is awesome! just wondering if anyone knows if there’s a React version of this sort of thing? Otherwise I might have crack at making one.Thanks for building this, Captain Tympanus 🙂