Smooth Parallax Scrolling

by kirupa   |   1 July 2013

For many practical reasons, our UIs are designed to be two dimensional. Once you throw in meaningful content, some navigation, and other doo-dads to make your application usable, adding any more dimensions simply gets in the way and becomes a distraction. Despite the stranglehold two dimensions have on what we create, there are subtle and effective ways of sneaking an extra dimension in here and there.

In this deconstruction, I will show you one effective way where you can simulate fake 3d by implementing something known as the parallax effect. Before I bore you by explaining what parallax is, let's look at an example instead. In the following iframe, scroll the content by using your mouse wheel or the scrollbars:

Notice what is happening to the background while you are scrolling. Your content seems to be scrolling at a faster rate than the background image. This variation in speed gives off the illusion of depth. It makes it look like your background is far away in the distance, and it makes it look like your content is nearby. Bam! That's some sweet parallax at work here.

In the following many sections, you will not only re-create this parallax effect, but you will also learn all about how it works the way it does so that you can take advantage of it in your own projects.

Onwards!

What Exactly is Parallax?

Generically speaking, parallax is the name for the illusion where objects' positions seem to be shifted based on the angle you are viewing them in. Have you ever noticed when you are driving that things further away seem to move slower than things closer to you? That's an example of parallax where your orientation to your surroundings affected your perception of how far things moved.
 
To learn more about Parallax, head on over to the ultimate authority on all things parallax, Wikipedia!

Overview of How this Effect Works

Before we start looking at the sweet implementation that makes our example rock, let's take a few steps back and talk about how exactly this effect works. As you will see in a few moments, how this effect works is pretty simple. It just takes advantage of a few techniques that you normally don't see being used together in this context.

Basically, for our parallax effect, we need a way to independently move our background image and content when the entire page is scrolled. The way we do this is by simulating a background image by layering an image element directly behind our content:

layering an image behind the content

By going with a separate element, there are several nice advantages. First, your content will not interfere in any way with your image. Second, you can squeeze a lot of performance by doing this - something I will discuss towards the end of this deconstruction. Lastly (and most importantly), with this arrangement, when you scroll the browser window, the content's and image's positions can be adjusted independently very easily:

independent adjustment

Speaking of which, let's look at the scroll behavior itself. There are three ways you can scroll content in a browser. One way is by using the scrollbars. Another way is by using the mouse wheel. The third way is by using the up / down / PgUp / PgDn / etc. keys on your keyboard.

Regardless of how you scroll your page, when you scroll, everything on your page moves up or down by default. The amount they move may vary depending on whether you used the scrollbar, mouse wheel, or keyboard, but rest assured that everything on your page will move. The first thing to do is to disable our image element from scrolling automatically:

no scroll for you

That is something that can easily be done in CSS as you will see later. Now, with our image's position fixed, scrolling the page will not result in the image scrolling as well:

background never moves

This means we can adjust our background's position as we see fit. The way we do that is by keenly listening for every scroll event our browser fires and then reacting to that event by shifting our image:

a lot of events

While this seems simple, there are some tricks involved in listening to and reacting to the scroll events in a way that is performant. Just keep that little warning under your hat for now, for we'll look at that in greater detail in a bit.

Next, how far we shift the image depends on how the scroll was initiated. Scrolling via the scrollbar requires less work because you only scroll as far as you drag the scroll bar. The keyboard falls under a similar boat and is pretty easy to handle. Now, scrolling via the mouse wheel requires some special handling because the mouse wheel scrolls by large increments. If we did nothing for the mouse wheel, the scrolling and subsequent parallax effect will look jerky. We want everything to animate smoothly.

All right! At this point, we painted our parallax effect with a pretty broad brush and highlighted some of the key points we need to be aware of. In the next few sections, we'll shift towards the implementation and how the HTML, CSS, and JavaScript bring everything you've seen here to life.

The Example

Now that you have a mental understanding of how this effect works, the next step is to get this example working on your own computer. Go ahead and create a new HTML document and copy/paste everything you see in the following gray box into it:

<!DOCTYPE html>
<html>
 
<head>
<meta content="en-us" http-equiv="Content-Language">
<meta charset="utf-8">
<meta content="A high performance parallax scrolling example." name="description">
<meta content="Parallax Scrolling Example" name="title">
<title>Parallax Scrolling Example</title>
<style>
body {
	padding: 45px;
	background-color: #010001;
}
p {
	font-family: Arial, Helvetica, sans-serif;
	font-size: 32px;
	line-height: 40px;
	padding: 30px;
	margin-right: 60px;
	color: #FFFFFF;
}
p span {
	background-color: rgba(1, 0, 1, .85);
}
a {
	color: #AFDBF2;
}
h1 {
	text-transform: capitalize;
	font-family: "Franklin Gothic Medium", "Arial Narrow", Arial, sans-serif;
	font-size: 40px;
	padding: 10px;
	margin: 0px;
	background-color: rgba(178, 45, 0, .75);
	color: #EEE;
}
#parallaxContainer {
	left: 0;
    position: fixed;
    top: 0;
    width: 100%;
    z-index: -1;
}
#parallaxContainer img {
	width: 100%;
	height: auto;
}
</style>
</head>
 
<body>
<div id="parallaxContainer">
	<img src="//www.kirupa.com/images/blurry_bg.jpg">
</div>
<h1>Some Random Content</h1>
<p><span>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin id lectus 
auctor, laoreet ante non, blandit magna. Aenean molestie dolor urna, id viverra 
diam dictum ac. Nulla facilisi. Maecenas sit amet facilisis ante. Pellentesque 
dignissim sed nibh sit amet iaculis. Sed convallis laoreet lorem eu euismod. 
Pellentesque sagittis, neque in blandit consectetur, leo nisl luctus nisi, vel 
volutpat neque lacus vel risus. Nam et tellus sed erat aliquam bibendum a in 
massa. Vivamus consequat dui nec neque feugiat molestie. Quisque eu leo at dui 
sodales bibendum. Nullam eget velit quis enim lobortis ultrices vel facilisis 
nibh.</span></p>
<p><span>Phasellus viverra nibh sed mi iaculis lacinia. Nullam quis risus 
tellus. Cras condimentum eleifend augue, a pellentesque tellus fermentum in. 
Fusce laoreet nulla vel enim mattis, ut molestie augue fringilla. Vestibulum 
lobortis eros velit, nec porttitor lorem tempus nec. Aliquam rutrum, tortor in 
elementum mollis, arcu ante consequat nunc, at dapibus velit ligula sed arcu. 
Proin euismod odio sed augue sagittis, sodales sollicitudin risus facilisis. 
Fusce lorem ante, volutpat et ligula nec, sodales aliquam turpis.</span></p>
<p><span>Aliquam pellentesque purus ac venenatis mollis. Duis lobortis consectetur sem 
a vehicula. Aenean dignissim eros ipsum, id pretium tellus dapibus id. Maecenas 
posuere risus eget quam feugiat, vel euismod nisl imperdiet. Vivamus malesuada 
nulla eu massa tristique tincidunt. Aliquam id nunc et sapien ultricies 
suscipit. Maecenas magna lorem, blandit quis risus sit amet, sagittis porttitor 
mauris. Aliquam sed vestibulum metus. Aenean malesuada vulputate mi eget 
venenatis. Quisque bibendum aliquam ligula, non vulputate mauris sodales 
condimentum. In lorem nibh, mollis eu orci eget, eleifend viverra erat. Nam 
elementum, quam ut sagittis lobortis, lacus ante luctus quam, eget convallis sem 
diam ut lorem.</span></p>
<p><a href="//www.kirupa.com/" target="_parent">Return to kirupa.com</a>.</p>

<script src="//www.kirupa.com/prefixfree.min.js"></script>
<script>

var requestAnimationFrame = window.requestAnimationFrame || 
                            window.mozRequestAnimationFrame || 
                            window.webkitRequestAnimationFrame ||
                            window.msRequestAnimationFrame;

var transforms = ["transform", 
				  "msTransform", 
				  "webkitTransform", 
				  "mozTransform", 
				  "oTransform"];
				  
var transformProperty = getSupportedPropertyName(transforms);

var imageContainer = document.querySelector("#parallaxContainer");

var scrolling = false;
var mouseWheelActive = false;

var count = 0;
var mouseDelta = 0;

//
// vendor prefix management
//
function getSupportedPropertyName(properties) {
    for (var i = 0; i < properties.length; i++) {
        if (typeof document.body.style[properties[i]] != "undefined") {
            return properties[i];
        }
    }
    return null;
}

function setup() {
	window.addEventListener("scroll", setScrolling, false);
	
	// deal with the mouse wheel
	window.addEventListener("mousewheel", mouseScroll, false);
    window.addEventListener("DOMMouseScroll", mouseScroll, false);
	
	animationLoop();
}
setup();

function mouseScroll(e) {
	mouseWheelActive = true;
	    
    // cancel the default scroll behavior
    if (e.preventDefault) {
    	e.preventDefault();
    }
    
    // deal with different browsers calculating the delta differently
    if (e.wheelDelta) {
    	mouseDelta = e.wheelDelta / 120;
    } else if (e.detail) {
    	mouseDelta = -e.detail / 3;
    }
}

//
// Called when a scroll is detected
//
function setScrolling() {
	scrolling = true;
}

//
// Cross-browser way to get the current scroll position
//
function getScrollPosition() {
    if (document.documentElement.scrollTop == 0) {
        return document.body.scrollTop;
    } else {
        return document.documentElement.scrollTop;
    }
}

//
// A performant way to shift our image up or down
//
function setTranslate3DTransform(element, yPosition) {
	var value = "translate3d(0px" + ", " + yPosition + "px" + ", 0)";
    element.style[transformProperty] = value;
}

function animationLoop() {
	// adjust the image's position when scrolling
	if (scrolling) {
		setTranslate3DTransform(imageContainer, 
								-1 * getScrollPosition() / 2);
		scrolling = false;
	}
	
	// scroll up or down by 10 pixels when the mousewheel is used
	if (mouseWheelActive) {
		window.scrollBy(0, -mouseDelta * 10);
		count++;
		
		// stop the scrolling after a few moments
		if (count > 20) {
			count = 0;
			mouseWheelActive = false;
			mouseDelta = 0;
		}
	}
		
	requestAnimationFrame(animationLoop);
}

</script>
</body>
</html>

Once you've added all of this HTML, CSS, and JavaScript, save your document and preview in your browser to make sure everything works as it does in the example you saw earlier. If you have a high resolution screen, you may need to resize your browser window to ensure you can get some scrollable content. (I'm pretty sure you didn't need me telling you that :P)

Now, as always, take a few moments and just look over everything. Try to link what you know about how this example works with the pieces of HTML, CSS, and JavaScript that you see. After you've glanced through everything for a few moments, let's go through everything together.

Deconstructing the Example

Keep all of the stuff you pasted visible, for here comes the fun part of going through each major section of code and seeing how it all fits together.

The HTML

Let's start at the top with our HTML. With the large chunk of sample lorem ipsum text removed, our HTML looks as follows:

<div id="parallaxContainer">
	<img src="//www.kirupa.com/images/blurry_bg.jpg">
</div>
<h1>Some Random Content</h1>
<p><span>Lorem ipsum dolor sit amet...
	.
	.
	.
	.
</span></p>
<p><a href="//www.kirupa.com/" target="_parent">Return to kirupa.com</a>.</p>

<script src="//www.kirupa.com/prefixfree.min.js"></script>

The most important part of the HTML is clearly our image and its container:

<div id="parallaxContainer">
	<img src="//www.kirupa.com/images/blurry_bg.jpg">
</div>

As you can see, this image is made up of a standard img element, and since it is a minor, it is kept under the watchful eye of a div element whose id is parallaxContainer.

Positioning and Sizing the Image

The styling of this example is defined entirely inside the style tags found towards the top of the document. Most of the style rules are pretty self-explanatory, so I'm going to skip over them. The relevant style rules that I do want to look at are the following two that directly affect our image:

#parallaxContainer {
	left: 0;
	position: fixed;
	top: 0;
	width: 100%;
	z-index: -1;
}
#parallaxContainer img {
	width: 100%;
	height: auto;
}

One of the things we want is for our image to appear behind all of our content. Not only that, we also don't want our image to automatically slide up and down when the browser is scrolled. Both of these things are taken care off inside the #parallaxContainer style rule:

#parallaxContainer {
	left: 0;
	position: fixed;
	top: 0;
	width: 100%;
	z-index: -1;
}

By setting the position to fixed and the top/left properties to 0, we force our image to stay anchored at the top-left of our page. Having our image appear directly below all of the content is handled by the z-index property which is set to -1.

There is another thing to notice about our background image. As you resize the window, the image maintains its aspect ratio while filling up as much space as possible. That behavior is specified in the #parallaxContainer img style rule:

#parallaxContainer img {
	width: 100%;
	height: auto;
}

By setting the width to 100%, we allow our image to take up as much horizontal space as its parent (the parallaxContainer) will allow. By setting the height to auto, the image's aspect ratio is preserved. You won't see our background image exhibiting any weird skewing. Pretty awesome, right?

At this point, we basically have the first stage of our example complete. Your image stays fixed on the top-left corner while your content happily scrolls up and down. Let's throw some of that scrolling happiness our image's way in the next section.

The JavaScript

The bulk of the action happens inside the script tag where our JavaScript lives. It is here where we transform our currently comatose background image into something that glides along with every scroll. This is going to be fun.

The Variable Declarations

Let's start at the very top with our variable declarations:

var requestAnimationFrame = window.requestAnimationFrame || 
                            window.mozRequestAnimationFrame || 
                            window.webkitRequestAnimationFrame ||
                            window.msRequestAnimationFrame;

var transforms = ["transform", 
				  "msTransform", 
				  "webkitTransform", 
				  "mozTransform", 
				  "oTransform"];
				  
var transformProperty = getSupportedPropertyName(transforms);

var imageContainer = document.querySelector("#parallaxContainer");

var scrolling = false;
var mouseWheelActive = false;

var count = 0;
var mouseDelta = 0;

Glance through these variables and see what they initialize to. Everything should be pretty straightforward. The transformProperty and transform may seem a little weird, but their sole reason for existence is to get us a vendor-specific hook into the transform property that looks a little bit like this:

object.style.transform = "...";

We'll revisit our transformProperty variable later, and it will make a whole lot more sense at that time. For now, just know that it exists and performs a valuable task.

The Entry Point

We are getting closer to the real action. After our variables, the code that gets run first is our setup function:

function setup() {
	window.addEventListener("scroll", setScrolling, false);
	
	// deal with the mouse wheel
	window.addEventListener("mousewheel", mouseScroll, false);
	window.addEventListener("DOMMouseScroll", mouseScroll, false);
	
	animationLoop();
}
setup();

This function is responsible for setting up the event listeners and calling our animation loop. There are three distinct events for the windows scroll (scroll) and mouse wheel (mousewheel, DOMMouseScroll) that we need to deal with, so we have three event listeners for each of these events:

function setup() {
	window.addEventListener("scroll", setScrolling, false);
	
	// deal with the mouse wheel
	window.addEventListener("mousewheel", mouseScroll, false);
	window.addEventListener("DOMMouseScroll", mouseScroll, false);
	
	animationLoop();
}

The last thing this function does is call the animationLoop function. Let's look at our animation loop next.

The Animation Loop: Part 1

The animationLoop function is responsible for creating the requestAnimationFrame callback that we rely on for animating things in code. It looks as follows:

function animationLoop() {
	// adjust the image's position when scrolling
	if (scrolling) {
		setTranslate3DTransform(imageContainer, 
								-1 * getScrollPosition() / 2);
		scrolling = false;
	}
	
	// scroll up or down by 10 pixels when the mousewheel is used
	if (mouseWheelActive) {
		window.scrollBy(0, -mouseDelta * 10);
		count++;
		
		// stop the scrolling after a few moments
		if (count > 20) {
			count = 0;
			mouseWheelActive = false;
			mouseDelta = 0;
		}
	}
		
	requestAnimationFrame(animationLoop);
}

Notice that this loop doesn't do much initially. There are two if statements that check whether the scrolling and mouseWheelActive variables are set to true. The last line is the requestAnimationFrame call to itself to ensure the animationLoop function gets called around 60 times a second.

At this point, your application is in a holding pattern and waiting further instructions. Your variables are globally just floating around, your event listeners are eagerly listening for events, and our animation loop is just looping without doing anything. All of this is great, but let's add some excitement into the mix.

Scrolling the Window / The Animation Loop: Part II

Let's say that you decide to interact with your browser's scrollbar and start scrolling the document. What we are going to do is walk through all of the code that gets hit in translating your browser scroll into something that shifts our background image. I will warn you - there are quite a number of steps here, but as long as you know where you are jumping, you should be fine.

First, when you start scrolling using the scrollbar, your browser will fire off a series of scroll events. When the scroll event is fired, our event listener that was declared in our setup function will overhear it:

function setup() {
	window.addEventListener("scroll", setScrolling, false);
	
	// deal with the mouse wheel
	window.addEventListener("mousewheel", mouseScroll, false);
	window.addEventListener("DOMMouseScroll", mouseScroll, false);
	
	animationLoop();
}

The moment our event listener overhears the scroll event, it calls the setScrolling function that acts as our event handler. This function looks as follows:

function setScrolling() {
	scrolling = true;
}

Pretty exciting, right? What the setScrolling function does is very simple. When it gets called, it sets the scrolling variable to true. While this seems like a simple and trivial operation, this results in something quite epic.

In our animation loop, about 1/60th of a second later, the following if statement that was waiting for the scrolling variable to become true now wakes up:

function animationLoop() {
	// adjust the image's position when scrolling
	if (scrolling) {
		setTranslate3DTransform(imageContainer, 
								-1 * getScrollPosition() / 2);
		scrolling = false;
	}
	
	// scroll up or down by 10 pixels when the mousewheel is used
	if (mouseWheelActive) {
		window.scrollBy(0, -mouseDelta * 10);
		count++;
		
		// stop the scrolling after a few moments
		if (count > 20) {
			count = 0;
			mouseWheelActive = false;
			mouseDelta = 0;
		}
	}
		
	requestAnimationFrame(animationLoop);
}

Let's look at just the highlighted portion in isolation:

if (scrolling) {
	setTranslate3DTransform(imageContainer, 
							-1 * getScrollPosition() / 2);
	scrolling = false;
}

The first thing we do inside this if statement is call the very important setTranslate3DTransform function. This function takes two arguments, and the two arguments we pass in are a pointer to our imageContainer element and the result of evaluating -1 * getScrollPosition() / 2. Let's make sense of that a bit.

The setTranslate3DTransform function looks as follows:

function setTranslate3DTransform(element, yPosition) {
	var value = "translate3d(0px" + ", " + yPosition + "px" + ", 0)";
    element.style[transformProperty] = value;
}

This function is what is responsible for shifting our image up and down depending on the direction you are scrolling in. It accomplishes that by setting the translate3D function's vertical position on our image element's transform CSS property. (Try repeating that five times!)

To better help explain what it does, imagine this is the CSS you are wanting to set:

#parallaxContainer {
	transform: translate3d(0px, 45px, 0px);
	-webkit-transform: translate3d(0px, 45px, 0px);
	-moz-transform: translate3d(0px, 45px, 0px);
	-ms-transform: translate3d(0px, 45px, 0px);
	-o-transform: translate3d(0px, 45px, 0px);
}

Now, translate this CSS into JavaScript and for the "y" part of the translate3d function's argument, you pass in a value as opposed to hard-coding a 45px. The setTranslate3DTransform function is the JavaScript conversion of this CSS - right down to the vendor prefixing which is handled by the transformProperty!

There is one more important thing to discuss with the setTranslate3DTransform function. The second argument we pass in, like I mentioned earlier, is the result of evaluating -1 * getScrollPosition() / 2. This expression is what helps offset our image's position from the rest of the page. Crucial to that is our getScrollPosition function:

function getScrollPosition() {
    if (document.documentElement.scrollTop == 0) {
        return document.body.scrollTop;
    } else {
        return document.documentElement.scrollTop;
    }
}

This function returns the pixel value of how far your document has scrolled from the top of the page. What we are doing is halving that value to slow down our background image's ascent or descent when scrolling. We also multiply that value by -1 to play nicely with what our translate3d function expects. The end result is that our setTranslate3DTransform function shifts our background image by exactly half of how much the rest of the page has scrolled.

So far, we've spent quite a bit of time looking at the setTranslate3DTransform function. Stepping all the way back to our animation loop and our if statement, the last thing we do after calling setTranslate3DTransform is set our scrolling variable to false:

if (scrolling) {
	setTranslate3DTransform(imageContainer, 
							-1 * getScrollPosition() / 2);
	scrolling = false;
}

What this means is that the next time our animation loop gets called, this if statement will no longer evaluate to true. What's going on here? Why would anybody do this? Well...my reason for doing this may seem a bit puzzling, but let me reuse a diagram that I used earlier:

a lot of events

When you are scrolling your document, you are not seeing a single scroll event get fired. Your browser fires a ridiculous amount of scroll events. It fires soooo many events, that you'll still get a smooth scroll going by setting the scrolling variable to false each time our animation loop gets called anyway. The reason is that our setScrolling function (aka the scroll event handler) sets the scrolling variable to true almost immediately:

function setScrolling() {
	scrolling = true;
}

As long as you are scrolling, your animation loop will ensure that the next time a repaint/redraw needs to happen, the smooth scrolling will still occur. This wouldn't be necessary if there was a scrollStopped or equivalent event that would let the animation loop know when to stop scrolling. In the absence of such an event, which really makes a lot of sense, this is one good workaround.

Why Not Update the Image Position Inside setScrolling?

If you noticed, there is a fair amount of indirection going on here. You scroll on the scrollbar. The scrolling variable is changed to true. Your animation loop notices the change and then starts to move your background image by calling setTranslate3DTransform. Why not cut out the middle man and have the call to setTranslate3DTransform directly live inside the setScrolling function? That would free you from having to deal with the scrolling variable and the animation loop altogether.

The reason has to go back to the number of times your scroll events get fired. Your setScrolling function will get overwhelmed by the large bursts of events that get generated when you are scrolling. You never want any code that updates the screen to get called in a situation like that. Remember, your browser redraws your screen 60 times a second...on a good day. Flooding your browser's drawing queue because your setScrolling event handler got called a bazillion times is a wasteful thing to do.

By keeping all logic relating to drawing/updating your screen inside your requestAnimationFrame loop, you avoid unnecessary calculations related to getting stuff to show up on the screen. That's why the setTranslate3DTransform call is in our animation loop as opposed to living inside setScrolling.

Phew! I don't think this section could have gotten any more long-winded even if I tried. If I were you, I'd take a short break by jumping on a sofa and taking a nap...kinda like this guy:

nap time!

If you are like me and are too lazy to find a sofa, placing your head gently on a desk works just as well.

Dealing with the Mouse Wheel

Like the extended version of a Lord of the Rings movie, we are not done yet! The last thing we are going to look at in our code is how to deal with the mouse wheel. Scrolling using the mouse wheel is a little weird and requires special handling. The reason is that each mouse wheel "scroll" doesn't scroll your content by a handful of pixels. Each scroll results in your page jumping, and each jump results in some jittery animation when your background image is being positioned at the right location. Just like what we did with the scrollbar scrolling in the previous section, let's walk through our code with what happens when you use your mouse wheel. Let's get started!

The moment you use your mouse wheel, depending on which browser you are using, either the mousewheel or DOMMouseWheel event will fire. As you saw earlier, our setup function already specified event listeners that are ready to react:

function setup() {
	window.addEventListener("scroll", setScrolling, false);
	
	// deal with the mouse wheel
	window.addEventListener("mousewheel", mouseScroll, false);
	window.addEventListener("DOMMouseScroll", mouseScroll, false);
	
	animationLoop();
}

When one of these two mouse wheel events are fired, both event listeners call the mouseScroll function to handle it. This function looks as follows:

function mouseScroll(e) {
	mouseWheelActive = true;
	    
    // cancel the default scroll behavior
    if (e.preventDefault) {
    	e.preventDefault();
    }
    
    // deal with different browsers calculating the delta differently
    if (e.wheelDelta) {
    	mouseDelta = e.wheelDelta / 120;
    } else if (e.detail) {
    	mouseDelta = -e.detail / 3;
    }
}

While this function may look a little imposing, what it does is pretty simple to explain. The first thing this function does is set the mouseWheelActive variable to true. We'll look at the fallout of this shortly, but for now, let's just keep moving down the rest of the code in this function.

Next, one of the most important things this function does is stop your mouse wheel scroll from actually scrolling the page. That's right! This major act of interference is made possible thanks to the following three highlighted lines:

function mouseScroll(e) {
	mouseWheelActive = true;
	    
    // cancel the default scroll behavior
    if (e.preventDefault) {
    	e.preventDefault();
    }
    
    // deal with different browsers calculating the delta differently
    if (e.wheelDelta) {
    	mouseDelta = e.wheelDelta / 120;
    } else if (e.detail) {
    	mouseDelta = -e.detail / 3;
    }
}

The reason for doing this is to override the default scrolling behavior with one of our own. We can't do that if we are competing with the browser's default scrolling behavior as well. Somebody has to give in, and that somebody ain't gonna be us!

Finally, the last thing this function does is set the value of the mouseDelta variable:

function mouseScroll(e) {
	mouseWheelActive = true;
	    
    // cancel the default scroll behavior
    if (e.preventDefault) {
    	e.preventDefault();
    }
    
    // deal with different browsers calculating the delta differently
    if (e.wheelDelta) {
    	mouseDelta = e.wheelDelta / 120;
    } else if (e.detail) {
    	mouseDelta = -e.detail / 3;
    }
}

The result of this chunk of code running is that our mouseDelta variable will, depending on the direction you are scrolling, store either a 1 or -1. You'll see the rationale behind why this is done in a few seconds when we return to our animation loop and scroll the page as a result of the mouse wheel being used.

The Animation Loop: Part III

And....we are back to our animationLoop function. This time, we are back because of something we did in the mouseScroll function earlier. When the mouse wheel is used, the mouseWheelActive variable is set to true. This means, about 1/60th of a second later, the following highlighted code in our animationLoop function becomes active:

function animationLoop() {
	// adjust the image's position when scrolling
	if (scrolling) {
		setTranslate3DTransform(imageContainer, 
								-1 * getScrollPosition() / 2);
		scrolling = false;
	}
	
	// scroll up or down by 10 pixels when the mousewheel is used
	if (mouseWheelActive) {
		window.scrollBy(0, -mouseDelta * 10);
		count++;
		
		// stop the scrolling after a few moments
		if (count > 20) {
			count = 0;
			mouseWheelActive = false;
			mouseDelta = 0;
		}
	}
		
	requestAnimationFrame(animationLoop);
}

What the highlighted code does is pretty simple. Remember, we disabled the browser's default reaction to the mouse scroll. We are re-creating it ourselves. Instead of the browser's mechanical jump to a new scroll position, we want to smoothly animate to the new position instead.

The first thing we do is scroll our window by either 10 pixels up or 10 pixels down:

if (mouseWheelActive) {
	window.scrollBy(0, -mouseDelta * 10);
	count++;
	
	// stop the scrolling after a few moments
	if (count > 20) {
		count = 0;
		mouseWheelActive = false;
		mouseDelta = 0;
	}
}

The direction is determined entirely by our mouseDelta variable, and the actual scroll position is set by the always-awesome window.scrollBy function. This highlighted line of code keeps rapidly getting called until our count variable increments beyond 20:

if (mouseWheelActive) {
	window.scrollBy(0, -mouseDelta * 10);
	count++;
	
	// stop the scrolling after a few moments
	if (count > 20) {
		count = 0;
		mouseWheelActive = false;
		mouseDelta = 0;
	}
}

Once this happens, around .3 seconds after you used the mouse wheel, the count variable is reset to 0, the mouseWheelActive function is set back to false, and mouseDelta is set to 0. Those are not the important details, though. What is important is that your window scrolled smoothly to the new position. Where there is a window scroll, there is a scroll event or two that gets fired. This causes our setScrolling function and all of the window scrolling code you saw earlier to become alive. That is how using the mouse wheel to scroll results in not only a smooth animation to the new scroll position, your background image smoothly repositions itself as well.

That is pretty freaking awesome!

A Word About Performance

Before wrapping this deconstruction up, I mentioned at the very beginning that the approach described in this tutorial is performant. I didn't justify it anywhere in this tutorial nor contrast it with other approaches. Now that we are done, it seems like a worthy topic to briefly resurrect.

Approach #1: What You Saw

One of the things I mentioned in my Animating Movement Smoothly Using CSS tutorial is that, for the best performance, push as much drawing/animation work to the GPU. That's not a hard thing to do. The easiest way to do that is by relying on the transform property's translate3d function for all position-related operations. The approach used in our example, as you recall, does this. That's why we are good.

Approach #2: Hi, background-position!

An alternate approach for creating a parallax effect involves setting the background CSS property and specifying your image as part of it. This is by far the easiest way of getting your background image displaying in your document, and you can alter the position by setting the background-position property. The reason why I don't like this approach is that the background-position property is not hardware accelerated. On a fast device, both the approach shown here as well as using background-position will work just fine. As always, when you go on the lower powered handheld devices, the background-position approach starts showing its flaws.

Approach #3: It's Canvas Time!

There is yet another approach you can use. This one involves layering a canvas element behind your content and specifying your background image as part of what you draw directly into your canvas. This approach is fast, and probably even faster than the first translate3d approach. The reason why I don't like this is because of the complexity involved. Working with the canvas 2d APIs is more involved than relying on the DOM. You are not only dealing with repositioning your background-image, you are also dealing with ensuring it is sized appropriately with your browser window size as well. When you add in all of the effort needed, don't use this approach unless you are really sure the performance benefits are truly worth it.

Better Performance Analysis by Paul Lewis

Paul Lewis pointed me to a fantastic performance-related article he wrote on HTML5Rocks called Parallaxin'. If you want a much MUCH better look at these three parallax scrolling approaches and their advantages/pitfalls, check out his article...now!

Getting Help

If you have questions, need some assistance on this topic, or just want to chat - post in the comments below or drop by our friendly forums (where you have a lot more formatting options) and post your question. There are a lot of knowledgeable and witty people who would be happy to help you out

Share

Did you enjoy reading this and found it useful? If so, please share it with your friends:

If you didn't like it, I always like to hear how I can do better next time. Please feel free to contact me directly via e-mail, facebook, or twitter.

Brought to you by...

Kirupa Chinnathambi
I like to talk a lot - A WHOLE LOT. When I'm not talking, I've been known to write the occasional English word. You can learn more about me by going here.

Add Your Comment (or post on the Forums)

blog comments powered by Disqus

Awesome and high-performance web hosting!
BACK TO TOP
new books - yay!!!