Multi-Layer Page Reveal Effects

A tutorial on how to create multi-layer page reveal effects with a couple of ideas for inspiration.

Today we’d like to show you how to create a simple and modern page transition effect. Maybe you have spotted some of the cool effects seen on sites like Nation or Keep Portland Weird, where two or more layers of overlays are animated across the page to reveal some new content. Let’s see how we can do something like that. We’ll focus on the animation part only, not the loading of content. For the effects we’ll be using CSS Animations.

Attention: We’ll be using CSS Animations and viewport units which are only supported in modern browsers.

The Markup

For our simple demo, we’ll create a full width and height layout with some arrows that will trigger the overlay animations. The idea is to show a page and in the middle of an overlay animation, switch to another page. While we will be just working with static content, you could plug your dynamic solution in here that fetches some data on the fly.

Our markup looks as follows:

<body>
	<main class="container">
		<div class="pages">
			<div class="page page--current">
				<!-- intro page content -->
			</div><!-- /page -->
			<div class="page">
				<!-- some other page -->
			</div>
		</div><!-- /pages -->
		<nav class="pagenav">
			<!-- buttons that will trigger the overlay animations -->
		</nav>
	</main>
</body>

The structure for the overlay will be inserted with JavaScript. We’ll work with a division that is placed as a fixed element on top of everything else. Depending on the direction, we’ll give that element some classes that will help us apply styles properly. The overlay will contain a number of layers that we will be able to define. Each of the layers will have a background color that we’ll set and also a specific animation in our stylesheet.

One example for how the “revealer” might look:

<div class="revealer revealer--right">
	<div class="revealer__layer"></div>
	<div class="revealer__layer"></div>
	<div class="revealer__layer"></div>
</div>

Each layer will additionally have its background color set. In order to create unique effects, we want to animate each layer individually. For that we set the parent initially to an off-screen position and then animate the layers in. This initial setting of the main element is done in our script (see below). The animation behavior of the layers is then defined in our stylesheet depending on the effect class, along with other styles for the page to be revealed.

The CSS

First, let’s take a look at some general styles for our body, container and pages (vendor prefixes omitted):

html,
body {
	min-height: 100vh;
	overflow-x: hidden;
}

.js .container {
	position: relative;
	height: 100vh;
	overflow: hidden;
}

.js .pages {
	position: relative;
	overflow: hidden;
	width: 100vw;
	height: 100vh;
	z-index: 0;
}

.page {
	padding: 6.5em;
	background: #66c6ff;
	display: flex;
	flex-direction: column;
}

.js .page {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	visibility: hidden;
	z-index: 1;
}

.js .page--current {
	visibility: visible;
	position: relative;
}

We want our page to be full width and height and hide any overflow. By default, and with JS (we use Modernizr), the pages are hidden and the current class sets the respective one to be visible. We want to make sure that our layout is functional without JavaScript, so we add the “conditional” styles.

Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.

Let’s see how the style for the revealer looks like. So we want to place the revealer on top of everything, with a fixed position:


.revealer {
	width: 100vw;
	height: 100vh;
	position: fixed;
	z-index: 1000;
	pointer-events: none;
}

Depending on which direction we are currently using, we need to set some initial positioning styles. In our script, as we will see in a bit, we will set some other transforms in order to insure that the top side of our revealer is always the one facing towards the screen. This will simplify the effects because the inner layer animations will always be the same (moving upwards), so we don’t need to define new animations for each direction if the parent is rotated and positioned correctly:

.revealer--cornertopleft,
.revealer--cornertopright,
.revealer--cornerbottomleft,
.revealer--cornerbottomright {
	top: 50%;
	left: 50%;
}

.revealer--top,
.revealer--bottom {
	left: 0;
}

.revealer--right,
.revealer--left {
	top: 50%;
	left: 50%;
}

.revealer--top {
	bottom: 100%;
}

.revealer--bottom {
	top: 100%;
}

The layers will have a default background color which will be then targeted individually for each effect in a dynamic way:

.revealer__layer {
	position: absolute;
	width: 100%;
	height: 100%;
	top: 0;
	left: 0;
	background: #ddd;
}

Next, let’s check out an example effect. The three layer effect adds an animation to each layer with a special easing function. The anim–effect class is added to the body so that we know which effect we are currently using. The revealer–animate is given to the revealer in order to trigger the animations.

As we can see here, we are adding the same animation properties to all the layers, except for the animation-name. With three different animations, we define the delays of each within the keyframes. Like that we ensure that all animations end in the same moment but allow for different appearance timings:

.anim--effect-3 .page:nth-child(2) {
	background: #F3A3D3;
}

.anim--effect-3 .revealer--animate .revealer__layer {
	animation: anim-effect-3-1 1.5s cubic-bezier(0.550, 0.055, 0.675, 0.190) forwards;
}

.anim--effect-3 .revealer--animate .revealer__layer:nth-child(2) {
	animation-name: anim-effect-3-2;
}

.anim--effect-3 .revealer--animate .revealer__layer:nth-child(3) {
	animation-name: anim-effect-3-3;
}

@keyframes anim-effect-3-1 {
	0% {
		transform: translate3d(0, 0, 0);
	}
	25%, 75% {
		transform: translate3d(0, -100%, 0);
		animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
	}
	100% {
		transform: translate3d(0, -200%, 0);
	}
}

@keyframes anim-effect-3-2 {
	0%, 12.5% {
		transform: translate3d(0, 0, 0);
	}
	37.5%, 62.5% {
		transform: translate3d(0, -100%, 0);
		animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
	}
	87.5%, 100% {
		transform: translate3d(0, -200%, 0);
	}
}

@keyframes anim-effect-3-3 {
	0%, 25% {
		transform: translate3d(0, 0, 0);
		animation-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1.000);
	}
	75%, 100% {
		transform: translate3d(0, -200%, 0);
	}
}

What we do here is to make the first layer move first but then “pause” in the middle until 75% while on top of the visible screen. Then we move it to the opposite direction to reveal the new content. The same happens for the other two layers, just that here we start them at different keyframes, decreasing the “pause”. The last layer does not have a pause at all, it just goes through to the other side, starting at 25%.

The JavaScript

We did a little plugin that will create the revealer depending on some options. The default options are the following:


Revealer.prototype.options = {
	// total number of revealing layers (min is 1)
	nmbLayers : 1,
	// bg color for the revealing layers
	bgcolor : '#fff',
	// effect classname
	effect : 'anim--effect-1',
	// callback
	onStart : function(direction) { return false; },
	// callback
	onEnd : function(direction) { return false; }
};

The function for adding the revealer with its layers and respective effect class looks as follows:

Revealer.prototype._addLayers = function() {
	this.revealerWrapper = document.createElement('div');
	this.revealerWrapper.className = 'revealer';
	classie.add(bodyEl, this.options.effect);
	var  strHTML = '';
	for(var i = 0; i < this.options.nmbLayers; ++i) {
		var bgcolor = typeof this.options.bgcolor === 'string' ? this.options.bgcolor : (this.options.bgcolor instanceof Array && this.options.bgcolor[i] ? this.options.bgcolor[i] : '#fff');
		strHTML += '
<div style="background:' + bgcolor + '" class="revealer__layer"></div>
';
	}
	this.revealerWrapper.innerHTML = strHTML;
	bodyEl.appendChild(this.revealerWrapper);
};

The most crucial function sets the initial positioning of our reveal element and adds the control classes for triggering the animation and for the chosen effect.

Depending on which direction we choose, we need to make sure that the reveal element gets the right transforms. Remember, we are simply rotating and positioning the revealer in a way so that the top side is always faced towards the screen so that the layers always move upwards. For the corner cases we need to make sure that the width and height of the revealer is set properly to the diagonal of the page. For the left and right case, we need to make sure that the height of the revealer is the width of the screen because we are rotating it 90 degrees.

Revealer.prototype.reveal = function(direction, callbacktime, callback) {
	// if animating return
	if( this.isAnimating ) {
		return false;
	}
	this.isAnimating = true;
	// current direction
	this.direction = direction;
	// onStart callback
	this.options.onStart(this.direction);

	// set the initial position for the layers´ parent
	var widthVal, heightVal, transform;
	if( direction === 'cornertopleft' || direction === 'cornertopright' || direction === 'cornerbottomleft' || direction === 'cornerbottomright' ) {
		var pageDiagonal = Math.sqrt(Math.pow(winsize.width, 2) + Math.pow(winsize.height, 2));
		widthVal = heightVal = pageDiagonal + 'px';
		
		if( direction === 'cornertopleft' ) {
			transform = 'translate3d(-50%,-50%,0) rotate3d(0,0,1,135deg) translate3d(0,' + pageDiagonal + 'px,0)';
		}
		else if( direction === 'cornertopright' ) {
			transform = 'translate3d(-50%,-50%,0) rotate3d(0,0,1,-135deg) translate3d(0,' + pageDiagonal + 'px,0)';
		}
		else if( direction === 'cornerbottomleft' ) {
			transform = 'translate3d(-50%,-50%,0) rotate3d(0,0,1,45deg) translate3d(0,' + pageDiagonal + 'px,0)';
		}
		else if( direction === 'cornerbottomright' ) {
			transform = 'translate3d(-50%,-50%,0) rotate3d(0,0,1,-45deg) translate3d(0,' + pageDiagonal + 'px,0)';
		}
	}
	else if( direction === 'left' || direction === 'right' ) {
		widthVal = '100vh'
		heightVal = '100vw';
		transform = 'translate3d(-50%,-50%,0) rotate3d(0,0,1,' + (direction === 'left' ? 90 : -90) + 'deg) translate3d(0,100%,0)';
	}
	else if( direction === 'top' || direction === 'bottom' ) {
		widthVal = '100vw';
		heightVal = '100vh';
		transform = direction === 'top' ? 'rotate3d(0,0,1,180deg)' : 'none';
	}

	this.revealerWrapper.style.width = widthVal;
	this.revealerWrapper.style.height = heightVal;
	this.revealerWrapper.style.WebkitTransform = this.revealerWrapper.style.transform = transform;
	this.revealerWrapper.style.opacity = 1;

	// add direction and animate classes to parent
	classie.add(this.revealerWrapper, 'revealer--' + direction || 'revealer--right');
	classie.add(this.revealerWrapper, 'revealer--animate');

	// track the end of the animation for all layers
	var self = this, layerscomplete = 0;
	this.layers.forEach(function(layer) {
		onEndAnimation(layer, function() {
			++layerscomplete;
			if( layerscomplete === self.options.nmbLayers ) {
				classie.remove(self.revealerWrapper, 'revealer--' + direction || 'revealer--right');
				classie.remove(self.revealerWrapper, 'revealer--animate');
				
				self.revealerWrapper.style.opacity = 0;
				self.isAnimating = false;

				// callback
				self.options.onEnd(self.direction);
			}
		});
	});
		
	// reveal fn callback
	if( typeof callback === 'function') {
		if( this.callbacktimeout ) {
			clearTimeout(this.callbacktimeout);
		}
		this.callbacktimeout = setTimeout(callback, callbacktime);
	}
};

And that’s all folks, we hope you enjoyed this little effect and get inspired.

References and Credits

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 in the loop: Get your dose of frontend twice a week

Fresh news, inspo, code demos, and UI animations—zero fluff, all quality. Make your Mondays and Thursdays creative!

Feedback 50

Comments are closed.
  1. Your work is always very beautiful and very functional continue your good work it is very inspiring

  2. I am a big fan of your work. thank you for all your posts
    I really appreciate it

  3. I’m pretty much of a newbie and I couldn’t find a way to use this in conjunction with smoothState.js. Would you give me some tips, pretty please?

  4. Amazing…. Can we use

    widthVal = winsize.height + "px"; heightVal = winsize.width + "px";

    instead of

    widthVal = '100vh' heightVal = '100vw';

    for cross browser support?

  5. I’ve tried to use this on my shopify page but can’t seem to get it to work..
    think it’s because it uses a custom modernizr.. if you get a chance, or if you have any idea on how to work a way around the problem.. i’ll appreciate any help..
    other than that, your work is amazing 😀

  6. Great tutorial! Is there a way to implement this between actual pages and not just sections within a page? If someone could point me in the right direction, I’d be very thankful!

  7. It’s pretty painful to read all this and then not be able to add more than one slide/page 🙁

    • You can do it by changing:

      onStart : function(direction) { ... var nextPage = pages[currentPage === 0 ? 0 : currentPage]; ... }, onEnd : function(direction) { ... var nextPage = pages[currentPage === 0 ? pages.length -1 : 0]; ... } ... function reveal(direction) { ... currentPage = currentPage < pages.length -1 ? currentPage+1 : 0; ... }

    • @Barbara I tried the solution that you post under and it works. But it’s just an ordered navigation, is there a way to direct every icon to an specific page and not just randomly?
      Thanks

    • I was just wondering where/how I should be editing this segment in. I was playing around with it and cant get it to work 😐

  8. Hey is it that we can apply transitions to a change <a href=”I change page.html” rel=”nofollow”> I change page </a> ?

    thank you

  9. Wow this is absolutely magnificent effect, really need to implmant that in some wordpress themes 🙂

  10. very cool stuff! thanks a lot.

    in the css part, the “animation-timing-function” property is repeated in two places:
    – as the second value of the “animation” property (short form)
    – in the @keyframe rule

    I understand the first one, but I can’t find an other example where it is used inside an @keyframe rule.
    Could you explain why it is here?
    Thank you

  11. Wonderful tutorial!! All these themes, design are coded for WordPress and this design is a crucial importance and the strategic use of Presentation is virtually perceptible by touch impression in a scheme that reflects a professional edge and a form that is more attractive to the Viewers than a simple inbound without changes.

  12. Hi! Amazing work!
    can somebody please tell me if and how its possible to import this into a wordpress template?

    thanks a lot!

  13. I can’t figure out how to get this to work with more than two pages. Any tips?

  14. Wow, what a great multi-layer page reveal effects
    Thanks for sharing the tutorial, great work!

  15. A wonderful article and a fantastic piece of code. It’s incredible how such a little movement and utilization of layers can hugely change the way in which users navigate a website. It is UI that is at the heart of any web design and I believe these little elements can make all the changes . . .

  16. Thanks, Pedro. The link is working now. Very good tutorial and good explanation for the example.

  17. How would one go about using this for a home page pre-loading animation and just fire this on page load?

  18. This is an amazing plugin man I been looking for this. I have a question is it possible to integrate “Content Move” effect with fullpage.js which is a one page scroll which can be found here http://www.alvarotrigo.com/fullPage/#firstPage.

    I really need this for a school project and to be clear all I am trying to achieve is the ” Content Move” effect to be activated on scroll when scrolling Up and Down?

    Thanks in advance

  19. Awesome work!

    Pls tell us how to link transition arrows to other index.html, to show different content on each arrow 😀

    • I also was wondering how to do this so that a menu can be created, Does anyone know how to do this?

  20. tanks for the great tutorial. but how can I point the buttons to specific page?

  21. Hello, thank you for a plugin, it is a good job.
    If you don’t mind I have a question, could you help me, please?
    I have tried to connect my navbar and logo(homepage) clicks to this plugin, but have a problem with the code, it will be great to solve this problem, Thank you very much.
    Here is the code by which I connected navbar and logo(homepage) to the plugin.

    var pages = [].slice.call(document.querySelectorAll(‘.pages > .page’)),
    currentPage = 0,

    revealerOpts = {
    // the layers are the elements that move from the sides
    nmbLayers : 3,
    // bg color of each layer
    bgcolor : [‘#52b7b9’, ‘#ffffff’, ‘#53b7eb’],
    // effect classname
    effect : ‘anim–effect-3’
    };
    revealer = new Revealer(revealerOpts);

    // clicking the page nav
    var navbar = document.getElementById(“navbar”).getElementsByTagName(“a”);
    for (var i = 0; i < navbar.length; i++) {
    navbar[i].addEventListener('click', function() {n = i; reveal('top'); });
    }

    // triggers the effect by calling instance.reveal(direction, callbackTime, callbackFn)
    function reveal(direction) {
    var callbackTime = 750;

    callbackFn = function() {

    // this is the part where is running the turning of pages
    classie.remove(pages[currentPage], 'page–current');

    currentPage = n;

    classie.add(pages[currentPage], 'page–current');

    };

    revealer.reveal(direction, callbackTime, callbackFn);
    }

  22. how can we have more than two page which navigates to there by a menu?

    Please help!!!!!!!