This article demonstrates how to use CSS transforms, perspective and some scaling trickery to create a pure CSS parallax scrolling website.
If you find this article useful and want to explore CSS Parallax further, you may find my follow-up article "Practical CSS Parallax" an interesting read.
Parallax is almost always handled with JavaScript and, more often than not, it's implemented badly with the worst offenders listening for the scroll
event and modifying the DOM directly in the handler, triggering needless reflows and paints. All this happens out of sync with the browsers rendering pipeline causing dropped frames and stuttering. It's not all bad though, requestAnimationFrame
and deferring DOM updates can transform parallax websites - but what if you could remove the JavaScript dependency completely?
Deferring the parallax effect to CSS removes all these issues and allows the browser to leverage hardware acceleration resulting in almost everything being handled by the compositor. The result is consistent frame rates and perfectly smooth scrolling. You can also combine the effect with other CSS features such as media queries or supports - responsive parallax anyone?
The theory
Before we dive into how the effect works, let's establish some barebones markup:
And here are the basic style rules:
The parallax
class is where the parallax magic happens. Defining the height
and perspective
style properties of an element will lock the perspective to its centre, creating a fixed origin 3D viewport. Setting overflow-y: auto
will allow the content inside the element to scroll in the usual way, but now descendant elements will be rendered relative to the fixed perspective. This is the key to creating the parallax effect.
Next is the parallax__layer
class. As the name suggests, it defines a layer of content to which the parallax effect will be applied; the element is pulled out of content flow and configured to fill the space of the container.
Finally we have the modifier classes parallax__layer--base
and parallax__layer--back
. These are used to determine the scrolling speed of a parallax element by translating it along the Z axis (moving it farther away, or closer to the viewport). For brevity I have only defined two layer speeds - we'll add more later.
Depth correction
Since the parallax effect is created using 3D transforms, translating an element along the Z axis has a side effect - its effective size changes as we move it closer to or farther away from the viewport. To counter this we need to apply a scale()
transform to the element so that it appears to be rendered at its original size:
The scale factor can be calculated with 1 + (translateZ * -1) / perspective
. For example, if our viewport perspective
is set to 1px
and we translate an element -2px
along the Z axis the correction scale factor would be 3:
Controlling layer speed
Layer speed is controlled by a combination of the perspective and the Z translation values. Elements with negative Z values will scroll slower than those with a positive value. The further the value is from 0
the more pronounced the parallax effect (i.e. translateZ(-10px)
will scroll slower than translateZ(-1px)
).
Parallax sections
The previous examples demonstrated the basic techniques using very simple content but most parallax sites break the page into distinct sections where different effects can be applied. Here's how to do that.
Firstly, we need a parallax__group
element to group our layers together:
Here's the CSS for the group element:
In this example, I want each group to fill the viewport so I've set height: 100vh
, however arbitrary values can be set for each group if required. transform-style: preserve-3d
prevents the browser flattening the parallax__layer
elements and position: relative
is used to allow the child parallax__layer
elements to be positioned relative to the group element.
One important rule to keep in mind when grouping elements is, we cannot clip the content of a group. Setting overflow: hidden
on a parallax__group
will break the parallax effect. Unclipped content will result in descendant elements overflowing, so we need to be creative with the z-index
values of the groups to ensure content is correctly revealed/hidden as the visitor scrolls through the document.
There are no hard and fast rules for dealing with layering as implementations will differ between designs. It's much easier to debug layering issues if you can see how the parallax effect works - you can do that by applying simple transform to the group elements:
Have a look at the following example - note the debug option!
Browser support
- Firefox, Safari, Opera and Chrome all support this effect.
- Firefox works too but there is currently a minor issue with alignment.
- IE doesn't support
preserve-3d
yet (it's coming) so the parallax effect won't work. That's ok though, you should still design your content to work without the parallax effect - You know, progressive enhancement and all that!
Update: 25 Feb 2015
Since writing this article it's become apparent that webkit based browsers don't correctly calculate the effective width of an element once transformed into 3D space and scaled up. This bug allows users to scroll content horiziontally ignoring any constraits defined with overflow
property. It is possible to work around for this bug by anchoring the transform-origin
and perspective-origin
to the right hand side of the viewport:
Locking the perspective and transform origins in this way causes any content overflow to occur off the left side of the screen. Since it's not possible to scroll to a negative position the overflow issue is masked.
I think YOU shared a lot of wisdom here. ;) Very impressive. Have a look at my site liebdich.biz for an implementation with oldschool Javascript
scroll
and the connected problems.Yours also works with touchscreens!! Which usually gets the scroll-position only on
touchend
. Love!!The demo works silky smooth on chrome , but in ff it lags pretty badly. Tested on a galaxy s4+. In case you find that interesting. Thanks a ton for the demo!
Thanks Ali. I don't have access to many mobile test devices so any feedback like this is really useful – which version of FF are you running?
Works silky smooth on lg g3. Will be planning with this idea today at work thanks!
I got here from reddit, on my phone (Nexus 5). At first I was disappointed because the effect did not work at all within the browser in my Reddit client.
I then tried in Chrome (36 for Android 4.4.4) where it worked almost beautifully.
Here is a screenshot of why "almost"... imgur.com/1Wtll2H
I checked in Chrome on my desktop and it seems that middle mouse button scrolling produces the same result... imgur.com/CTcDMTI
Opera (of course) seems to run into the same problem. I had no issues with Firefox, though: very smooth and didn't let me scroll off the side of the page.
Perhaps worth noting that I could only scroll to the right side. Not the left.
Thanks for the cool demo!
It works perfectly fine in the latest Firefox build for me.
Raphaël, thanks for reporting those issues. The horizontal scrolling seems to be triggered when scaling the elements up - I'll see if that can be countered.
With the Reddit client on your Nexus 5- did you just see a white page? if so that could be because `vh` units aren't supported. I've added a fixed height fallback - could you try the demo again?
Is it possible that the Chrome MobBrowser is applying some styles that we need to get rid of, before trying to go CSS Pure Parallax?
Hello,
The horizontal scrolling is caused by a long date known issue on Chrome. I found a way to remove the scroll, but it only applies to some specific cases, where the parallax effect is desired on a full page, as it uses position: fixed. I add a wrapper and add a the following CSS rules:
Also, you could add the following rule to make available the .parallax__layer--back div height customisation:
Hope it helps.
Bruno Sabot you Sir are a genius.
Nice! Thanks for writing this up. A few more notes:
codepen.io/…/bHEcA is where I first found this technique. I believe Scott adapted the technique by Keith. The demo in that pen has a nice 3d view of parallax on two-axises. Plus, Scott's created a mixin that automates most of the work for you. Really handy.
codepen.io/…/JycFw by Keith has a more flashy demo of this technique.
frozenrockets.nl/…/parallax has a good example of your standard masthead parallax. It could use some layer promotion to avoid paint storms on non-retina but it looks great.
One of these days we should hash out credit of this technique for those who care about those things. I believe we both found the underlying technique independently but the addition of scale for depth correction is critical and that is all Keith, so Paul, feel free to call it the Keith technique in future presentations. @brnnbrn) helped me get the math behind the scaling correct and repeatable at various perspective values although it’s deceptively simple as explained above in the article.
I get really excited about implementations beyond depth transforms. Adding rotation to the mix adds dimensionality to elements themselves, not just the page. If you do want to add a bit of JS into this technique you can simply change perspective based on device orientation or mouse position for some other cool effects.
Thanks for the shout out, Scott! Keith, really excited to be seeing more on this technique. I'd love to help out if you're both interested in working further on it.
what do you mean by layer promotion?
Thank you for sharing this... good article Keith... and thank you Paul Irish for the links :)
Amazing! Always wanted something that worked well on iPads!
Lags in Firefox 31 under Mac OS X 10.9.4
I'm not seeing that.
FF 31 / OSX 10.9.2 is ok here.
You are probably on a retina display ... performances aren't so great with CSS3 transitions.
The demo does not work in Internet Explorer 11.0.9600 on Windows 8.1.
I tried it in Chrome and see the desired effects, but it's totally broken in IE 11.
Yep. I covered that the the browser support section of the article. IE11 or below doesn't support
transform-style: preserve-3d
.IE is the one that's broken
How can I properly explain the creative team and designers how progressive enhancement works. Something which sticks. Usually these discussions between developers and creatives end up with them saying "well, just make it in JS I guess".
"I can use CSS to make that work, but it won't work in IE" "So just use JS" "But CSS will make it like, super smooth!" "But not in IE, its still xx% of the paying visitors" "You could design something that works in IE, and then visualize the scrolly bits for modern browsers" "Nah, we want parallax in all browsers. And we don't have the resources/hours/monies to put effort into it."
Sadface!
Very great work. Thank you for sharing!
Is it possible to incorporate a CSS based parallax scrolling page through a Wordpress theme?
Sure, the effect is achieved with CSS so you can apply the technique your Wordpress theme if you so desire.
You're my new personal hero! Thanks a bunch!
I'm running Firefox version 30.0 here, and no lagging noticed at all.
Hi! This is so amazing! a real parallax effect with no JS! great work dude!! thanks for sharing this with the Internet!
Final demo works 100% on Sony Xperia and Chrome. Slight side-effect, you can scroll to the right causing contents to move out of sight.
Great stuff!
Thanks for sharing! We've been putting off parallax effects since touchscreens don't execute scroll-attached interactions until
touchend
. This is a revelation for developers who don't want their progressive scroll enhancements to be lost on the mobile crowd.OTOH, it strikes me that many are misusing the term 'parallax' to describe *scroll-activated depth effects*. Parallax is a phenomenon that stems from having two eyes, so it requires two lines of sight to occur. How far away is that galloping horse? Your brain can use the nearby fence and the distant tree to help you estimate that by comparing how your left and right eyes line up the three objects differently.
Much of what we call 'parallax' on the web would require your eyes to be remounted on the vertical axis (forehead and chin?). And even then there is rarely a focal object with a foreground and background. Can we find a new term? Or is all lost? Are eggs truly dairy?
Don't get me wrong, I'll likely be implementing the "Keith Technique" (Scott said was okay) in my latest project. But I won't be calling it parallax.
Parallax is a result of linear perspective in motion and does not require stereo vision to be called parallax. This technique uses the term parallax correctly.
On BlackBerry Z10 v10.2.1.3247 it works smooth and glitch free. Well done!
This is really great but a question came to my mind. If we are translating the ZIndex of the fore\back-grounds so to create a perspective, as you said we will lose some dimension on those elements that have less zIndex.
You solved the problem by scaling the div that is on the background so that they are actually the same size, tricking perspective given by the CSS. Magnificent, and really smart. That said:
We are scaling something to 2x (in the above case) so that all elements retain the visual size they had prior to 3D transforms, which is fine, but the problem (if there is one) comes with the visual elements you are serving to the users; If everything you were parallaxing and scaling was text or SVG, no problem, that'd be awesome, but if you are scaling 2x an image that is supposed to look good on 1x, wouldn't you like like A LOT of quality on that image? The trick would be to serve the user with a 2x image (just like we do when working with 2x Pixel Ratio devices) but we would also charge more loading times and memory usage on the users, wouldn't we?
I await your response because I am eager to know more about your method!
Best of luck to you !
height=100vh isn't going to fly on iOS (bug), so don't forget to have some media queries to change that for iPad and smaller. Neat technique though, I can't wait to try it.
I usually use
html,body {height: 100%}
when using this technique and that does the trick with better browser support.Hey Keith thanks a bunch for this super easy to follow tutorial/explanation. I have one question though. I've encountered the following problem on the implementation inside my project: If I add a layer with translateZ(-3px) I need a scale of 4 to get the correct size of the element back. This works but on the other hand it enlarges my page (on the y-axis) for a unnecessary and weird looking large amount. I don't understand exactly why this is happening nor how to prevent it. Any ideas?
I hadn't noticed this side-effect until I published this post and stated receiving feedback. The issue seems to be the same thing that causes the horizontal scroll in webkit. It seems some browsers are calculating the scaled elements size based on the dimensions of an element in 2D space and not accounting for the 3D depth - that's what causing your strange page size issue.
My demo doesn't show this because the last slide doesn't feature an parallax elements.
Thanks Keith, for taking time to look into it. Your comment has given me the right hint to fix my problem. I set a max-height for the 'slides', have to reposition them afterwards, but the scale factor is correct and the page doesn't get larger than it should get.
Simply Amazing! Thanks for sharing!
I noticed when using Chromium and autoscrolling with a mouse, that the top demo works perfectly, but the depth-corrected demos allow scrolling vertically plus horizontally to the right. It looks pretty cool but probably is not the intended behaviour.
Great article and very clever ideas. How easily can this work on background images with background-position?
The parallax effect cannot be applied to individual properties like
background-position
.Very nice demo, Keith! Thanks for your hard work and taking the time to share.
Wow! Pure CSS parallax, very nice!
What i'm doing wrong? codepen.io/…/zmxpB I'm stuck for 2 hours already, i don't know why blocks not filling 100% height. it's making me crazy. I was repeating and exploring demo styles in browser many times, and still i'm missing something propably very small.
My code now works fine in chrome and opera (i beat that z-index), but i have a huge whitespace on the bottom in FF. This time i really don't know what to do.
Very, very cool! Thank you for sharing your wisdom:)
This makes for a very fast loading page.
Terry
Great effect, and some very clever thinking. On iOS though, you lose inertial scrolling - you can't flick-scroll it as it's stuck to your finger. Is there any way around this?
I have tried before and apparently, to increase performance both Android and iOS won’t allow any scripting during the momentum action. The good thing is that it is possible to disable this effect with media queries. The other way to go is to simulate/re-create the scrolling process on a div with javascript through `touchmove` and `touchend` events, but it doesn’t feel ideal to me. :)
Not that I can see... codepen.io/…/sLvrJ It completely breaks when you add the -webkit-overflow-scrolling property.
Had a good play with it and I love the idea not using javascript but pure css transitions to get the desired effect. Why? Because I am lazy and it makes developing so much easier by tweaking the css transition attributes in the browser in-built developer tools instead of having constantly upload changes, refresh and check. ;) Thanks a lot
This is one of the most clever CSS only techniques I have ever read about. Thanks so much for sharing Keith.
This approach is now broken in Chrome (37.0.2062.94). Still works in firefox though. I think it literally happened with the latest chrome update today. Luckily, my website where I implemented this is not in production yet. I'll update if I find a fix.
you can work around this by moving the
perspective
style rule to the<body>
elementThanks Keith for sharing this. I used this technique in a website ive build. It worked fine in almost all browsers (expect ie ofc). But now it seems that it stopped working in Chrome. When i look at you demo's on this page, the effect is gone as well...
Dit anyone notice a change in Chrome ?
Yeah, the effect broken in Chrome 37. I'm still trying to figure out what changed. You can fix the issue by moving the
perspective
style rule to a parent of the element that has the.parallax
class (i.e. the<body>
element)