This article demonstrates how to use CSS transforms, perspective and some scaling trickery to create a pure CSS parallax scrolling website.

Parallax is almost always handled with JavaScript and, more often than not, it's implemented badly with the worst offenders listenening for the scroll event, modifying the DOM directly in the handler and 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?

Try the demo

The theory

Before we dive into how the effect works, let's establish some barebones markup:

<div class="parallax">
  <div class="parallax__layer parallax__layer--back">
    ...
  </div>
  <div class="parallax__layer parallax__layer--base">
    ...
  </div>
</div>

And here are the basic style rules:

.parallax {
  perspective: 1px;
  height: 100vh;
  overflow-x: hidden;
  overflow-y: auto;
}
.parallax__layer {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}
.parallax__layer--base {
  transform: translateZ(0);
}
.parallax__layer--back {
  transform: translateZ(-1px);
}

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.

Try it

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:

.parallax__layer--back {
  transform: translateZ(-1px) scale(2);
}

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:

.parallax__layer--deep {
  transform: translateZ(-2px) scale(3);
}

Try it depth corrected

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:

<div class="parallax">
  <div class="parallax__group">
    <div class="parallax__layer parallax__layer--back">
      ...
    </div>
    <div class="parallax__layer parallax__layer--base">
      ...
    </div>
  </div>
  <div class="parallax__group">
    ...
  </div>
</div>

Here's the CSS for the group element:

.parallax__group {
  position: relative;
  height: 100vh;
  transform-style: preserve-3d;
}

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:

.parallax__group {
    transform: translate3d(700px, 0, -800px) rotateY(30deg);
}

Have a look at the following example - note the debug option!

Try it with groups

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!

Comments

  • DOC ASAREL

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

  • Ali

    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!

    • Keith Clark

      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?

    • Tom

      Works silky smooth on lg g3. Will be planning with this idea today at work thanks!

    • Raphaël Titsworth-Morin

      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!

    • John Doe

      It works perfectly fine in the latest Firefox build for me.

    • Keith Clark

      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?

    • Lorenzo P.

      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?

    • Bruno Sabot

      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:

      .parallax__layer--wrapper {
        bottom: 0;
        left: 0;
        overflow: hidden;
        position: fixed;
        right: 0;
        top: 0;
      }

      Also, you could add the following rule to make available the .parallax__layer--back div height customisation:

      .parallax__layer--back {
        height: 50%; /* Now the background is only 50% height */
        transform-origin: center 100%;
      }

      Hope it helps.

    • Israel Cruz

      Bruno Sabot you Sir are a genius.

  • Paul Irish

    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.

    • Scott Kellum

      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.

    • Brenna O'Brien

      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.

    • qwd

      what do you mean by layer promotion?

  • Wolfgang

    Thank you for sharing this... good article Keith... and thank you Paul Irish for the links :)

  • Daniel Sailes

    Amazing! Always wanted something that worked well on iPads!

  • misterd

    Lags in Firefox 31 under Mac OS X 10.9.4

    • Keith Clark

      I'm not seeing that.

    • Geoff

      FF 31 / OSX 10.9.2 is ok here.

    • Jean-François

      You are probably on a retina display ... performances aren't so great with CSS3 transitions.

  • Warren

    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.

    • Keith Clark

      Yep. I covered that the the browser support section of the article. IE11 or below doesn't support transform-style: preserve-3d.

    • Javier Villanueva

      IE is the one that's broken

    • Maurice

      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!

  • fabian

    Very great work. Thank you for sharing!

  • Timothee

    Is it possible to incorporate a CSS based parallax scrolling page through a Wordpress theme?

    • Keith Clark

      Sure, the effect is achieved with CSS so you can apply the technique your Wordpress theme if you so desire.

  • Ty

    You're my new personal hero! Thanks a bunch!

  • webSIGHTdesigns

    I'm running Firefox version 30.0 here, and no lagging noticed at all.

  • Erick M Acevedo

    Hi! This is so amazing! a real parallax effect with no JS! great work dude!! thanks for sharing this with the Internet!

  • Johan Pretorius

    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!

  • Kerry

    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.

    • Scott Kellum

      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.

  • Sam

    On BlackBerry Z10 v10.2.1.3247 it works smooth and glitch free. Well done!

  • Lorenzo P.

    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 !

  • pk

    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.

    • Scott Kellum

      I usually use html,body {height: 100%} when using this technique and that does the trick with better browser support.

  • verpixelt

    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?

    • Keith Clark

      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.

    • verpixelt

      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.

  • Matteo

    Simply Amazing! Thanks for sharing!

  • Tom

    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.

  • Steven Vachon

    Great article and very clever ideas. How easily can this work on background images with background-position?

    • Keith Clark

      The parallax effect cannot be applied to individual properties like background-position.

  • Dave Balmer

    Very nice demo, Keith! Thanks for your hard work and taking the time to share.

  • Sean

    Wow! Pure CSS parallax, very nice!

  • suez

    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.

    • suez

      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.

  • Terry

    Very, very cool! Thank you for sharing your wisdom:)

    This makes for a very fast loading page.

    Terry

  • Mike

    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?

    • Marcus

      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. :)

    • Jon Cousins

      Not that I can see... codepen.io/…/sLvrJ It completely breaks when you add the -webkit-overflow-scrolling property.

  • Torsten McFly

    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

  • Marcus

    This is one of the most clever CSS only techniques I have ever read about. Thanks so much for sharing Keith.

  • Riley

    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.

    • Keith Clark

      you can work around this by moving the perspective style rule to the <body> element

  • Dominic

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

    • Keith Clark

      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)