All in Perspective: Pure CSS Parallax and Other Scroll Effects

Follow along using these Codepens:

Basic Parallax
Fixed-Object Parallax

Despite claims that “parallax is dead,” the effect is very much alive and well — and captivating — when implemented correctly. Unfortunately, nine times out of ten, the implementation is botched in Javascript. Playing with scroll event listeners is risky business for performance, and modifying the DOM directly triggers unnecessary re-renders, causing choppy animations and sticky scrolls. Proper parallax can be pulled off using JS. Here’s an excellent article on how it should be done:

But for simpler scrolling animations, using pure CSS is a foolproof, performant approach.


The Basic Set-up

As a first example, we will create a page with a parallax header and static page content. Because we are ditching Javascript, we don’t have access to the window’s scroll position, and we don’t need it! The key to pulling off the effect is to take advantage of perspective. We will create 2 layers of content. The content we want to scroll slower, will be placed “further” away from the user on the z-axis. This will force the browser to do all of the heavy lifting for us.

Here’s the basic markup:

<div class="wrapper">
<div class="section parallax">
<h1>Heading</h1>
</div>
<div class="content">
<h1>Site Content</h1>
</div>
</div>

Let’s flesh out the CSS. We need to tell the browser to take advantage of perspective along the z-axis. We do this by adding the perspective property to our wrapper class:

perspective: 1px;

A larger perspective value will cause a greater difference in scroll speeds between the layers.

Next, we force the wrapper to take up 100% of the browser’s viewport and set overflow-y to auto. This enables content within the wrapper to scroll as usual, but scroll speed for descendants will now be relative to the wrapper’s perspective value:

.wrapper {
height: 100vh;
overflow-x: hidden;
overflow-y: auto;
perspective: 1px;
}

The first div will contain our header content. The background image, applied to a pseudo element, will be placed one pixel “away” from the user on the z-axis, while the content will be level with the rest of the page and will scroll at the normal speed.

Nothing too special happens in the .section class applied to the header. It defines the height and formats the content. Here’s the CSS:

.section {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
color: white;
}

All of the parallax goodness happens in the pseudo element:

.parallax::after {
content: " ";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
transform: translateZ(-1px) scale(2);
background-size: 100%;
z-index: -1;
background-image: url(some link to some image);
}

The pseudo element is placed behind the content of the header. translateZ(-1px) defines the layer’s distance from the user along the z-axis.

Because we are moving the layer further back, the contents appear smaller (think about what happens when you move an object away from you). To account for this, we must scale the layer back to size, using scale(2).

If your perspective is set to 1px, the formula to scale layers back to their default size is: 1 + (translateZ * -1) / perspective.

In our case, a translateZ(-2px) would require a scale(3) and so on…

Add some static content below the header and you’ll have a beautiful parallax effect with no JS needed!

Here is a link to the Codepen for this example.


Now For the Fun Part: Fixed-Object Parallax

Basic parallax is great. It breathes life into an otherwise static webpage. But you can do so much more with perspective in CSS. This became clear to me in working on a scroll animation for my portfolio site.

I wanted a stack of SVG lego bricks to break apart at different speeds as the user scrolled down my homepage. After fiddling around with JS for a while, I realized that this effect could be achieved with pure CSS — and be buttery smooth at that!

The idea is to create separate layers of objects within the master container, each with a different translateZ value (read: scroll speed). While implementing this, I quickly realized that in translating and scaling the objects, I had no way of keeping track of their x and y positions (they would change relative to the object’s translateZ value). To solve this, I wrapped each object in a clear container that fit the entire screen. I could then position the object precisely within the wrapper and apply the translateZ and scale to the wrapper instead of the object itself.

Only one .wrapper class is needed to define the size for all objects:

.object-wrapper {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: none;
justify-content: center;
}

Different speeds can then be defined and applied to the object wrappers:

.speed-1 {
transform: translateZ(-1px) scale(2);
}
.speed-2 {
transform: translateZ(-2px) scale(3);
}
.speed-3 {
transform: translateZ(-3px) scale(4);
}

Here is a Codepen demonstrating Fixed-Object Parallax:

Pure CSS offers a world of possibilities for animating content relative to the scroll position — and the best part is, in ditching JS, it’s nearly impossible to mess up performance!

When it comes to performant parallax, it really is all about perspective.