Parallax scrolling with JS controlled CSS variables

CSS custom properties (often referred to as CSS variables) are quite new, but powerful. They — as the name suggests — behave like CSS properties and aren’t just variables like the ones you know from SASS:

  • No preprocessor needed.
  • They inherit and behave like CSS properties.
  • You can access and manipulate them in JavaScript.

The last point is where it gets interesting. A combination of CSS and JS allows us to do the following:

header {
background: var(--color);
}
footer {
background: var(--color);
}

In JS we change --color to a random value. As we apply the property to the body, every property that uses --color changes. In real time!

document.documentElement.style.setProperty('--color', randomColor())

An exciting behavior, which was enough reason for me to build a parallax scrolling library powered by CSS variables. The next chapters will explain how you can take advantage of it.

Browser support

Three major browsers already support CSS variables: Chrome, Safari and Firefox. Edge and older browsers are out of luck. Is this a problem? No! Parallax animations are a perfect example for progressive enhancement. Your site keeps working everywhere, but users with the latest browsers get some special effects 😎.

Getting started

basicScroll has zero dependencies, CommonJS and AMD support and support for mobile and desktop. It basically includes everything you need to create stunning parallax animations. Download basicScroll from GitHub or install the library with Bower or npm to get started:

npm install basicscroll

HTML & CSS

In order to use the library we first need HTML and CSS. Lets create something similar to the Firewatch website. A hero with multiple layers. A composition of illustrations that creates a parallax scene.

Here’s the HTML …

<img class="scene" data-modifier="30" src="p0.png">
<img class="scene" data-modifier="18" src="p1.png">
<img class="scene" data-modifier="12" src="p2.png">
<img class="scene" data-modifier="8" src="p3.png">
<img class="scene" data-modifier="6" src="p4.png">
<img class="scene" data-modifier="0" src="p6.png">

… and the CSS …

body {
/* Let the user scroll */
height: 2000px;
/* Frontmost scene and background should have the same color */
background: black;
}
.scene {
position: absolute;
width: 100%;
transform: translateY(var(--translateY));
will-change: transform;
}

The .scene is where it gets interesting: We are going to use --translateY to change the position of each layer. The will-change CSS property provides a way to hint browsers about the kind of changes to be expected on an element. This way browser can setup appropriate optimizations ahead of time. A big performance win in our example, as we already know that transform will change when the users scrolls.

JavaScript

But without JS, nothing will happen 🤔 Our variable is currently empty and therefore won’t do anything. Browsers will ignore the property and use their default for transform. Here’s where basicScroll comes in. What we want from basicScroll is that …

  • … it starts to change the CSS custom property for each layer as soon as the user starts scrolling.
  • … it stops to change the CSS custom property for each layer as soon as the bottom of the layers reaches the top of the viewport.

basicScroll works with instances. Each instance tracks one element or has a fixed start and end position. What we need is an instance for each layer:

document.querySelectorAll('.scene').forEach((elem) => {

const modifier = elem.getAttribute('data-modifier')

basicScroll.create({
elem: elem,
from: 0,
to: 519,
props: {
'--translateY': {
from: '0',
to: `${ 10 * modifier }px`,
direct: true
}
}
}).start()

})

0 tells the library to start changing the props at 0px. 519 on the other hand specifies that we don’t want any changes when the user scrolled more than 519px. 519px is the height of our scenes, but could be whatever you want.

The props object contains the CSS properties that should change. In our case the --translateY variable. The layers should move faster the more they are in the background of the illustration. The frontmost layer should have the slowest transformation, while the rearmost layer should have the fastest. That’s exactly what the modifier is for.

And last but not least: The direct property, which tells basicScroll to apply the variable directly on the element.

Everything we need to do now is to activate the instance by calling it’s start function. That’s it! 🎉 Enjoy the full demo on CodePen.

Artwork by my co-worker @setgraphic: Check out his work.