Multi-Level Menu

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

From our sponsor: Looking for an intuitive whiteboard style project management tool? Give Shortcut a try for free.

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:

Mary Lou

ML is a freelance web designer and developer with a passion for interaction design. She studied Cognitive Science and Computational Logic and has a weakness for the smell of freshly ground peppercorns.

http://www.codrops.com

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

Feedback 70

Comments are closed.
    • 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.

  1. 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

  2. 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 🙂

  3. 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.

  4. 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!

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

  6. 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

  7. 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?

  8. 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.

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

  10. 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?

  11. 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.

  12. 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?