How to create parallax effects on scroll, in web design. By Andy Shora
← back to andyshora.com | Tweet this articleEver wondered how to achieve parallax effects on scroll? I'll show you how simple it can be, using CSS & a bit of jQuery. I'm not a fan of long intros, so feel free to scroll straight down for the demo!
Previously I've referred to scroll events. Unfortunately, we have to deal with the following problem: iOS devices freeze DOM manipulation during scroll, queuing them to apply when the scroll finishes.. This is going to result in extremely jerky animations if we rely on the position of these events. Instead, we have to tie our animation logic to touch events if the browser supports them.
As touchmove events stop when your finger leaves the screen, but the browser will continue to scroll for around a second as part of the 'slowing down' easing effect. For this reason, I've decided to bind both the touchmove event and the scroll event. The bug you'll see is when you swipe past the end of the container and release your touch before the brick has hit the bottom, it overflows the container, and position: relative is only applied when the queued scroll events are fired when the scroll transition has ended.
A solution for this problem (well, more of a workaround), is to design your parallax effects with this in mind. Consider placing an overlapping element with a higher z-index at the end point, so even if the brick overruns, it will be behind the higher element. If the overlapping element is tall enough, it should provide enough time for the scroll events to fire and position the brick relatively in the correct position.
Update: haven't been able to get this working correctly on iOS devices yet :-(
I've investigated using setInterval hacks to keep checking the scrollTop position of the window, but this feels dirty, and is still unreliable considering how infrequently the value is updated. RequestAnimationFrame is only available in iOS6, so stay tuned for an update for mobile performance, or feel free to help me out in the comments!
Update 2: Check out my demo showing the scroll event problems on mobile!
We'll set bounds on the parallax effect, so the brick always stays within it's container.
For this demo we are using a simple <div>, which has the following data attributes set which define the parallax effect (all values are in pixels):
<div class="parallax brick" data-id="brick-1" data-start="200" data-stop="1150"></div>
The styles are pretty normal, but note the top property will be updated for the parallax effect.
.brick { width: 100px; height: 38px; padding: 10px 0 0 0; margin: 0 auto; display: inline-block; position: relative; top: 0; z-index: 1; }
We need to initialise all of the parallax effects when the document is ready, to try and minimise the amount of processing and DOM interaction done during scrolling.
var parallaxElements = []; var windowHeight = 0; $(document).ready(function() { windowHeight = $(window).height(); $('html,body').scrollTop(1); // auto scroll to top // touch event check stolen from Modernizr var touchSupported = (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch); // if touch events are supported, tie our animation to the position to these events as well if (touchSupported) { $(window) .bind('touchmove', function(e) { var val = e.currentTarget.scrollY; parallax(val); }); } $(window) .bind('scroll', function(e) { var val = $(this).scrollTop(); parallax(val); }); // update vars used in parallax calculations on window resize $(window).resize(function() { windowHeight = $(this).height(); for (var id in parallaxElements) { parallaxElements[id].initialOffsetY = $(parallaxElements[id].elm).offset().top; parallaxElements[id].height = $(parallaxElements[id].elm).height(); } }); // get parallax elements straight away as they wont change // this will minimise DOM interactions on scroll events $('.parallax').each(function(){ $elm = $(this); var id = $elm.data('id'); // use data-id as key parallaxElements[id] = { id: $elm.data('id'), start: $elm.data('start'), stop: $elm.data('stop'), speed: $elm.data('speed'), elm: $elm[0], initialOffsetY: $elm.offset().top, height: $elm.height(), width: $elm.outerWidth() }; }); });
Our aim is to scroll down to the element (the red brick), and achieve the following behaviour:
function parallax(scrollTop) { for (var id in parallaxElements) { // distance of element from top of viewport var viewportOffsetTop = parallaxElements[id].initialOffsetY - scrollTop; // distance of element from bottom of viewport var viewportOffsetBottom = windowHeight - viewportOffsetTop; if ((viewportOffsetBottom >= parallaxElements[id].start) && (viewportOffsetBottom <= parallaxElements[id].stop)) { // element is now active, fix the position so when we scroll it stays fixed var speedMultiplier = parallaxElements[id].speed || 1; var pos = (windowHeight - parallaxElements[id].start); $(parallaxElements[id].elm) .css({ position: 'fixed', top: pos+'px', left: '50%', marginLeft: -(parallaxElements[id].width/2) +'px' }); } else if (viewportOffsetBottom > parallaxElements[id].stop) { // scrolled past the stop value, make position relative again $(parallaxElements[id].elm) .css({ position: 'relative', top: (parallaxElements[id].stop-parallaxElements[id].start)+'px', left: 'auto', marginLeft: 'auto' }); } else if (viewportOffsetBottom < parallaxElements[id].start) { // scrolled up back past the start value, reset position $(parallaxElements[id].elm) .css({ position: 'relative', top: 0, left: 'auto', marginLeft: 'auto' }); } } }
I hope you've enjoyed this tutorial. I'll add some more advanced effects in the coming weeks, introducing speed, direction and rotation to parallax animations. In the mean time, I would love you to share the f*ck out of this article, it's my first proper tutorial for a while now, and don't forget to hit me up on twitter! @andyshora