Multi-Level Menu

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

Blueprint_MutliLevelMenu

View demo Download source

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: '\00B7';
	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 (
    ´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 = ''; 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);

View demo Download source

Previous:
Next:

Tagged with:

Mary Lou (Manoela Ilic) 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.

View all contributions by

Website: http://tympanus.net/

Related Articles

CSS Reference

Learn about all important CSS properties from the basics with our extensive and easy-to-read CSS Reference.

It doesn't matter if you are a beginner or intermediate, start leanirng CSS now.

Feedback 47

    • 3

      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?

    • 5

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

    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.

    • 7

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

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

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

    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.

    • 13

      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()
      }

    • 14

      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.

  5. 15

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

  6. 16

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

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

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

    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?

Follow this discussion

Leave a Comment