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.
这是一个简单的多层次菜单,这个创意是每一个菜单项在一个级别改变后进行动画效果,动画从点击的项目开始,延迟传播。对于多级菜单的新级别的传入项目,动画延迟遵循相同的逻辑。我们有导航和返回按钮可选的元素(未显示在我们的演示)。使用数据属性引用更深层次。我们为菜单菜单添加了一些带有菜单切换的媒体查询,我们还提供了一个简单的回调示例。
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>
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);
}
}
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);