Page Stack Navigation

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

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);

Tagged with:

Manoela Ilic

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

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

Feedback 62

Comments are closed.
  1. your demo are so wonderful that they make me breaking my brain to find a good excuse to use them! Bravo Mary Lou!

  2. Wow, thanks Mary Lou! Two great items from you in the same week, just when I was suffering from ML withdrawal during your quiet spell. You never cease to amaze me with your creativity.

  3. You are one of the most talented people I’ve ever seen in my life. Great work! This is more than great

  4. Lovely idea, thanks for the demo & source 🙂
    I kept trying to figure out how to make use of the stack itself. I understood that I can click on a second or third page in a stack to open it up. However, I was unaware which page I was opening. Seems that the stack here is mostly a cosmetic feature, although as a user, I would expect it to give me a glimpse of page content (at least headings).

    I am working on a similar feature, where I’m trying to stack modals from left to right. In case of many items in a stack (like with deck of cards), user should be able to hover specific one (or pull the stack) to reveal enough to make a selection.

    Anyways, keep up the good work! 🙂

    • Maybe have a page move up a bit on hover would be nice to show a little preview. I was thinking the same thing.

    • Thank you sooo sooo much! I have been experiencing a terrible time with Safari and I just didn’t know what to do anymore. The site you suggested is just life saving. Thank you so much for pointing that out. I’m going to try and add a few more pages once you click on the menu but even as is, it’s enough.

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

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

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

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

    • Hi Cameron,

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

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

    • 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');.

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

    • 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;
      }

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

    • @Nik – Were you ever able to get this working? I am also trying to figure out how to link to other “pages” from within the content. (without sending user through the navigation).

    • Did you ever get this? Still trying to figure this out myself. JS not my strong-suit.

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

  13. Is there any way i can place links to internal pages into the section pages? not only in the nav menu

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

      Thanks

    • I am also trying to do this. Has anyone been able to figure out how to link to the other pages without using the nav?

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

  15. This doesn’t work at all in Internet Explorer 10 or 11, or in Safari 5.1.7 on Windows 7.

    Any ideas on how to make this backwards compatible?

    • UPDATE: Looks like I had to enable javascript to run in IE, in which case it works fine.

      Safari 5.1.7 doens’t work, though.

  16. Well, this does not work in the default Android browser. And some Safaris.

    Or is the page opens after a redirection. The main layout depedns on JS, this looks amazing, but not ready for production.

  17. Very nice! I would suggest ev.stopPropagation() after calling toggleMenu, took me a bit of time to understand why
    I couldn’t trigger the menu from a page content. Anyway very nice layout thx! 🙂

    • @Pa – Can you provide an example. I think I’m trying to do the same thing you’re suggesting, but I can’t get it to work.

  18. Did anyone ever find out how to link to one of the “pages” from within the content area. (so you don’t have to select the nav and then the item you’re trying to go to?)

  19. How could I modify the page such that it automatically hides the browsers Address bar (on mobile) once the page is loaded?

  20. Did anyone ever discover a proper fix to allow for vertical scrolling, whilst hiding inactive pages? The earlier suggestions are incomplete.