Page Stack Navigation

A template for a simple page stack navigation based on the Dribbble shot by Ilya Kostin, Stacked navigation.

BlueprintStacked

View demo Download source

This Blueprint is page navigation effect based on the Dribbble shot Stacked navigation by Ilya Kostin. The idea is to show a navigation when clicking on the menu button and transform all pages in 3D and move them to the bottom of the viewport. The next two pages are shown in the back of the current page as a stack. When clicking on a menu item, the respective page comes up.

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

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

The HTML

<!-- navigation -->
<nav class="pages-nav">
	<div class="pages-nav__item"><a class="link link--page" href="#page-home">Home</a></div>
	<div class="pages-nav__item"><a class="link link--page" href="#page-docu">Documentation</a></div>
	<div class="pages-nav__item"><a class="link link--page" href="#page-manuals">Manuals</a></div>
	<div class="pages-nav__item"><a class="link link--page" href="#page-software">Software</a></div>
	<div class="pages-nav__item"><a class="link link--page" href="#page-custom">Customization & Settings</a></div>
	<div class="pages-nav__item"><a class="link link--page" href="#page-training">Training</a></div>
	<div class="pages-nav__item pages-nav__item--small"><a class="link link--page link--faded" href="#page-buy">Where to buy</a></div>
	<div class="pages-nav__item pages-nav__item--small"><a class="link link--page link--faded" href="#page-blog">Blog & News</a></div>
	<div class="pages-nav__item pages-nav__item--small"><a class="link link--page link--faded" href="#page-contact">Contact</a></div>
	<div class="pages-nav__item pages-nav__item--social">
		<a class="link link--social link--faded" href="#"><i class="fa fa-twitter"></i><span class="text-hidden">Twitter</span></a>
		<a class="link link--social link--faded" href="#"><i class="fa fa-linkedin"></i><span class="text-hidden">LinkedIn</span></a>
		<a class="link link--social link--faded" href="#"><i class="fa fa-facebook"></i><span class="text-hidden">Facebook</span></a>
		<a class="link link--social link--faded" href="#"><i class="fa fa-youtube-play"></i><span class="text-hidden">YouTube</span></a>
	</div>
</nav>
<!-- /navigation-->
<!-- pages stack -->
<div class="pages-stack">
	<!-- page -->
	<div class="page" id="page-home">
		<!-- page content -->
	</div>
	<!-- /page -->
	<div class="page" id="page-docu">
		<!-- page content -->
	</div>
	<div class="page" id="page-manuals">
		<!-- page content -->
	</div>
	<div class="page" id="page-software">
		<!-- page content -->
	</div>
	<div class="page" id="page-custom">
		<!-- page content -->
	</div>
	<div class="page" id="page-training">
		<!-- page content -->
	</div>
	<div class="page" id="page-buy">
		<!-- page content -->
	</div>
	<div class="page" id="page-blog">
		<!-- page content -->
	</div>
	<div class="page" id="page-contact">
		<!-- page content -->
	</div>
</div>
<!-- /pages-stack -->
<button class="menu-button"><span>Menu</span></button>
<script src="js/classie.js"></script>
<script src="js/main.js"></script>

The CSS

html.js,
.js body {
	overflow: hidden;
	height: 100vh;
}

/* Pages nav */

.pages-nav {
	display: -webkit-flex;
	display: flex;
	-webkit-flex-wrap: wrap;
	flex-wrap: wrap;
	-webkit-justify-content: center;
	justify-content: center;
	-webkit-align-items: center;
	align-items: center;
	padding: 20px;
	text-align: center;
	background: #0e0f0f;
}

.js .pages-nav {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 50vh;
	padding: 30px;
	pointer-events: none;
	opacity: 0;
	background: transparent;
	-webkit-transition: -webkit-transform 1.2s, opacity 1.2s;
	transition: transform 1.2s, opacity 1.2s;
	-webkit-transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
	transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
	-webkit-transform: translate3d(0, 150px, 0);
	transform: translate3d(0, 150px, 0);
}

.js .pages-nav--open {
	pointer-events: auto;
	opacity: 1;
	-webkit-transform: translate3d(0, 0, 0);
	transform: translate3d(0, 0, 0);
}

.pages-nav__item {
	width: 33%;
	padding: 1em;
}

.js .pages-nav__item {
	padding: 0 10%;
}

.pages-nav .pages-nav__item--social {
	width: 100%;
	opacity: 0;
	-webkit-transition: -webkit-transform 1.2s, opacity 1.2s;
	transition: transform 1.2s, opacity 1.2s;
	-webkit-transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
	transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
	-webkit-transform: translate3d(0, 20px, 0);
	transform: translate3d(0, 20px, 0);
}

.pages-nav--open .pages-nav__item--social {
	opacity: 1;
	-webkit-transition-delay: 0.35s;
	transition-delay: 0.35s;
	-webkit-transform: translate3d(0, 0, 0);
	transform: translate3d(0, 0, 0);
}

.link {
	font-size: 0.85em;
	font-weight: bold;
	position: relative;
	letter-spacing: 1px;
	text-transform: uppercase;
}

.link:hover,
.link:focus {
	color: #fff;
}

.link--page {
	display: block;
	color: #cecece;
}

.link--page:not(.link--faded)::before {
	content: '';
	position: absolute;
	top: 100%;
	left: 50%;
	width: 30px;
	height: 2px;
	margin: 5px 0 0 -15px;
	background: #fff;
	-webkit-transition: -webkit-transform 0.3s;
	transition: transform 0.3s;
	-webkit-transform: scale3d(0, 1, 1);
	transform: scale3d(0, 1, 1);
}

.link--page:hover:before {
	-webkit-transform: scale3d(1, 1, 1);
	transform: scale3d(1, 1, 1);
}

.link--faded {
	color: #4f4f64;
}

.link--faded:hover,
.link--faded:focus {
	color: #5c5edc;
}

.link--page.link--faded {
	font-size: 0.65em;
}

.link--social {
	font-size: 1.5em;
	margin: 0 0.75em;
}

.text-hidden {
	position: absolute;
	display: block;
	overflow: hidden;
	width: 0;
	height: 0;
	color: transparent;
}

/* Pages stack */

.js .pages-stack {
	z-index: 100;
	pointer-events: none;
	-webkit-perspective: 1200px;
	perspective: 1200px;
	-webkit-perspective-origin: 50% -50%;
	perspective-origin: 50% -50%;
}

.js .page {
	position: relative;
	z-index: 5;
	overflow: hidden;
	width: 100%;
	height: 100vh;
	pointer-events: auto;
	background: #2a2b30;
	box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1);
}

.js .pages-stack--open .page {
	cursor: pointer;
	-webkit-transition: -webkit-transform 0.45s, opacity 0.45s;
	transition: transform 0.45s, opacity 0.45s;
	-webkit-transition-timing-function: cubic-bezier(0.6, 0, 0.4, 1);
	transition-timing-function: cubic-bezier(0.6, 0, 0.4, 1);
}

.js .page--inactive {
	position: absolute;
	z-index: 0;
	top: 0;
	opacity: 0;
}

/* page content */

.info {
	font-size: 1.25em;
	max-width: 50%;
	margin-top: 1.5em;
}

.poster {
	position: absolute;
	bottom: 4vh;
	left: 60%;
	max-width: 100%;
	max-height: 80%;
}

/* Menu button */

.menu-button {
	position: absolute;
	z-index: 1000;
	top: 30px;
	left: 30px;
	width: 30px;
	height: 24px;
	padding: 0;
	cursor: pointer;
	border: none;
	outline: none;
	background: transparent;
}

.no-js .menu-button {
	display: none;
}

.menu-button::before,
.menu-button::after,
.menu-button span {
	background: #5f656f;
}

.menu-button::before,
.menu-button::after {
	content: '';
	position: absolute;
	top: 50%;
	left: 0;
	width: 100%;
	height: 2px;
	pointer-events: none;
	-webkit-transition: -webkit-transform 0.25s;
	transition: transform 0.25s;
	-webkit-transform-origin: 50% 50%;
	transform-origin: 50% 50%;
}

.menu-button span {
	position: absolute;
	left: 0;
	overflow: hidden;
	width: 100%;
	height: 2px;
	text-indent: 200%;
	-webkit-transition: opacity 0.25s;
	transition: opacity 0.25s;
}

.menu-button::before {
	-webkit-transform: translate3d(0, -10px, 0) scale3d(0.8, 1, 1);
	transform: translate3d(0, -10px, 0) scale3d(0.8, 1, 1);
}

.menu-button::after {
	-webkit-transform: translate3d(0, 10px, 0) scale3d(0.8, 1, 1);
	transform: translate3d(0, 10px, 0) scale3d(0.8, 1, 1);
}

.menu-button--open span {
	opacity: 0;
}

.menu-button--open::before {
	-webkit-transform: rotate3d(0, 0, 1, 45deg);
	transform: rotate3d(0, 0, 1, 45deg);
}

.menu-button--open::after {
	-webkit-transform: rotate3d(0, 0, 1, -45deg);
	transform: rotate3d(0, 0, 1, -45deg);
}

@media screen and (max-width: 60em) {
	.info {
		max-width: 100%;
	}
	.poster {
		position: relative;
		top: auto;
		left: auto;
		display: block;
		max-width: 100%;
		max-height: 50vh;
		margin: 0 0 0 50%;
	}
	.pages-nav__item {
		width: 50%;
		min-height: 20px;
	}
	.link--page {
		overflow: hidden;
		white-space: nowrap;
		text-overflow: ellipsis;
	}
	.link--social {
		margin: 0 0.1em;
	}
}

@media screen and (max-width: 40em) {
	.js .pages-nav {
		display: block;
		padding: 10px 20px 0 20px;
		text-align: left;
	}
	.js .pages-nav__item {
		width: 100%;
		padding: 4px 0;
	}
	.js .pages-nav__item--small {
		display: inline-block;
		width: auto;
		margin-right: 5px;
	}
	.pages-nav__item--social {
		font-size: 0.9em;
	}
	.menu-button {
		top: 15px;
		right: 10px;
		left: auto;
	}
	.info {
		font-size: 0.85em;
	}
	.poster {
		margin: 1em;
	}
}

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 = { transitions: Modernizr.csstransitions },
		// transition end event name
		transEndEventNames = { 'WebkitTransition': 'webkitTransitionEnd', 'MozTransition': 'transitionend', 'OTransition': 'oTransitionEnd', 'msTransition': 'MSTransitionEnd', 'transition': 'transitionend' },
		transEndEventName = transEndEventNames[ Modernizr.prefixed( 'transition' ) ],
		onEndTransition = function( el, callback ) {
			var onEndCallbackFn = function( ev ) {
				if( support.transitions ) {
					if( ev.target != this ) return;
					this.removeEventListener( transEndEventName, onEndCallbackFn );
				}
				if( callback && typeof callback === 'function' ) { callback.call(this); }
			};
			if( support.transitions ) {
				el.addEventListener( transEndEventName, onEndCallbackFn );
			}
			else {
				onEndCallbackFn();
			}
		},
		// the pages wrapper
		stack = document.querySelector('.pages-stack'),
		// the page elements
		pages = [].slice.call(stack.children),
		// total number of page elements
		pagesTotal = pages.length,
		// index of current page
		current = 0,
		// menu button
		menuCtrl = document.querySelector('button.menu-button'),
		// the navigation wrapper
		nav = document.querySelector('.pages-nav'),
		// the menu nav items
		navItems = [].slice.call(nav.querySelectorAll('.link--page')),
		// check if menu is open
		isMenuOpen = false;

	function init() {
		buildStack();
		initEvents();
	}

	function buildStack() {
		var stackPagesIdxs = getStackPagesIdxs();

		// set z-index, opacity, initial transforms to pages and add class page--inactive to all except the current one
		for(var i = 0; i < pagesTotal; ++i) {
			var page = pages[i],
				posIdx = stackPagesIdxs.indexOf(i);

			if( current !== i ) {
				classie.add(page, 'page--inactive');

				if( posIdx !== -1 ) {
					// visible pages in the stack
					page.style.WebkitTransform = 'translate3d(0,100%,0)';
					page.style.transform = 'translate3d(0,100%,0)';
				}
				else {
					// invisible pages in the stack
					page.style.WebkitTransform = 'translate3d(0,75%,-300px)';
					page.style.transform = 'translate3d(0,75%,-300px)';		
				}
			}
			else {
				classie.remove(page, 'page--inactive');
			}

			page.style.zIndex = i < current ? parseInt(current - i) : parseInt(pagesTotal + current - i);
			
			if( posIdx !== -1 ) {
				page.style.opacity = parseFloat(1 - 0.1 * posIdx);
			}
			else {
				page.style.opacity = 0;
			}
		}
	}

	// event binding
	function initEvents() {
		// menu button click
		menuCtrl.addEventListener('click', toggleMenu);

		// navigation menu clicks
		navItems.forEach(function(item) {
			// which page to open?
			var pageid = item.getAttribute('href').slice(1);
			item.addEventListener('click', function(ev) {
				ev.preventDefault();
				openPage(pageid);
			});
		});

		// clicking on a page when the menu is open triggers the menu to close again and open the clicked page
		pages.forEach(function(page) {
			var pageid = page.getAttribute('id');
			page.addEventListener('click', function(ev) {
				if( isMenuOpen ) {
					ev.preventDefault();
					openPage(pageid);
				}
			});
		});

		// keyboard navigation events
		document.addEventListener( 'keydown', function( ev ) {
			if( !isMenuOpen ) return; 
			var keyCode = ev.keyCode || ev.which;
			if( keyCode === 27 ) {
				closeMenu();
			}
		} );
	}

	// toggle menu fn
	function toggleMenu() {
		if( isMenuOpen ) {
			closeMenu();
		}
		else {
			openMenu();
			isMenuOpen = true;
		}
	}

	// opens the menu
	function openMenu() {
		// toggle the menu button
		classie.add(menuCtrl, 'menu-button--open')
		// stack gets the class "pages-stack--open" to add the transitions
		classie.add(stack, 'pages-stack--open');
		// reveal the menu
		classie.add(nav, 'pages-nav--open');

		// now set the page transforms
		var stackPagesIdxs = getStackPagesIdxs();
		for(var i = 0, len = stackPagesIdxs.length; i < len; ++i) {
			var page = pages[stackPagesIdxs[i]];
			page.style.WebkitTransform = 'translate3d(0, 75%, ' + parseInt(-1 * 200 - 50*i) + 'px)'; // -200px, -230px, -260px
			page.style.transform = 'translate3d(0, 75%, ' + parseInt(-1 * 200 - 50*i) + 'px)';
		}
	}

	// closes the menu
	function closeMenu() {
		// same as opening the current page again
		openPage();
	}

	// opens a page
	function openPage(id) {
		var futurePage = id ? document.getElementById(id) : pages[current],
			futureCurrent = pages.indexOf(futurePage),
			stackPagesIdxs = getStackPagesIdxs(futureCurrent);

		// set transforms for the new current page
		futurePage.style.WebkitTransform = 'translate3d(0, 0, 0)';
		futurePage.style.transform = 'translate3d(0, 0, 0)';
		futurePage.style.opacity = 1;

		// set transforms for the other items in the stack
		for(var i = 0, len = stackPagesIdxs.length; i < len; ++i) {
			var page = pages[stackPagesIdxs[i]];
			page.style.WebkitTransform = 'translate3d(0,100%,0)';
			page.style.transform = 'translate3d(0,100%,0)';
		}

		// set current
		if( id ) {
			current = futureCurrent;
		}
		
		// close menu..
		classie.remove(menuCtrl, 'menu-button--open');
		classie.remove(nav, 'pages-nav--open');
		onEndTransition(futurePage, function() {
			classie.remove(stack, 'pages-stack--open');
			// reorganize stack
			buildStack();
			isMenuOpen = false;
		});
	}

	// gets the current stack pages indexes. If any of them is the excludePage then this one is not part of the returned array
	function getStackPagesIdxs(excludePageIdx) {
		var nextStackPageIdx = current + 1 < pagesTotal ? current + 1 : 0,
			nextStackPageIdx_2 = current + 2 < pagesTotal ? current + 2 : 1,
			idxs = [],

			excludeIdx = excludePageIdx || -1;

		if( excludePageIdx != current ) {
			idxs.push(current);
		}
		if( excludePageIdx != nextStackPageIdx ) {
			idxs.push(nextStackPageIdx);
		}
		if( excludePageIdx != nextStackPageIdx_2 ) {
			idxs.push(nextStackPageIdx_2);
		}

		return idxs;
	}

	init();

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

Feedback 48

  1. 3

    It would be awesome if in the stacked view you can see what the other pages behind the front one are. This way it would be easy to know what you are selecting from the stack too.

  2. 7

    Great design! However, I was testing on Safari (5.1.7)on Windows 7 and the navigation isn’t working properly. Have you a fix for this?

  3. 8

    This is brilliant! Awesome blueprint. Funny thing though, I tried to use it with only three pages and when I am on the second page, if I click the menu button again, the stack is broken and only displays 2 pages and in the wrong stacking order but when I am on the first and second, the stack looks good. Anyone have any ideas whats wrong?

  4. 9

    How would this be implemented within a multi-page layout? I love the UX, but the client needs a multi-page site for SEO.

    I want to avoid flicker issues with redrawing the page as much as possible, and I think that maybe the best way is after loading the selected divpage I instead load the separate page once the animation completes?

    • 10

      Hi Cameron,

      Did you find any solution for multi page with SEO friendly. Please share it if you find something.

  5. 12

    This is great! I’m playing around with the settings but I cannot make a section to scroll to show the rest of the content on it. Is there another way use it instead of the overflow=hidden?

    • 13

      If you want to allow your content pages to scroll, simply modify main.js and add page.style.overflow = 'scroll'; to line 80 right after classie.remove(page, 'page--inactive');.

  6. 14

    Is there anyway to increase length of the page? Currently it is equal to screen length of device. I want to make it scroll. Actually my idea is make a main landing page and use above nav just for linking to parts of my landing page.

    • 16

      A bit old but I sorted some of this out just with CSS. You’d need an additional class added to html.js & .js body with height: 100vh & overflow:hidden when the menu is open to prevent scrolling. And a way to hide the inactive pages which I haven’t sorted out yet.

      html.js, .js body {
      overflow: visible;
      height: auto;
      }

      .pages-stack–open .page {
      height: 100vh;
      }

      .js .page {
      position: relative;
      z-index: 5;
      overflow: hidden;
      width: auto;
      pointer-events: auto;
      background: #2a2b30;
      box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1);
      height: auto;
      min-height: 100vh;
      }

  7. 17

    Is there any way to implement anchors in the content that lead to a other page just like the anchors in the navigation? I want to place teasers in the content of one page that link to the other. But the script only slices the anchors from the URL if they are located in the navigation. So is there a way to change that?

  8. 18

    Hey guys,

    did anyone find out how to reduce the distance between the expanded menu and the stack preview in mobile view?

    Greatly appreciate any help,
    Daniel

    • 20

      Do you found how I can place links to internal pages into the section pages ?

      Thanks

  9. 22

    Hi there! Love your site and all the content everyone creates and shares.

    I have been playing around with this theme for a business site, and am running into an issue with changing the color of the Main Menu Bar Links. I have been able to change the base color and also the Focus color but this breaks the function of the button. I change the class of the button by copying from the existing class but I am thinking I am missing a part to copy and the result is the new class does not have all of the formatting needed from the CSS to function. Any help would be greatly appreciated!

Follow this discussion

Leave a Comment

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>