How to make Multi-Layered Parallax Illustration with CSS & Javascript

The Painting used for the parallax effect.

Hey friends, I will be showing you how to create a simple multi-layered illustration with depth that transitions into the content in a unique way. We’ll be using a technique that involves CSS and pure JS (no jQuery!).

The tutorial is for beginners with basic Javascript and CSS knowledge, so I will be explaining most of the things and link to external sources. (Edit: If you’re looking for information on how to start learning Javascript I wrote an article about it.)

Preview final effect

Prepare the illustration

Let’s start by cutting the painting into layers. Best-case-scenario would be a picture that you painted or if you have access to a source file with all the layers.

If you don’t, then do not worry we can fix that.

I will be providing a painting that I did last year. If you don’t feel like creating your own assets, you can download assets before we start for free.

Download assets

Visualized concept of layers in 3D Space

What we are looking for is splitting the painting into multiple png files with transparent background (layers), which will allow us to create the sense of depth.

Layers in the back will be moving slower than the ones in the foreground what will give us the effect of depth.

One other thing that will give a better transition to next section is making the bottom of the image the same color as the background. I have added a slight foreground line on the bottom and made sure that it’s not transparent on the bottom.

The Version on the right has a slight line added that will give us better transition into next section.

Get codin’


What you will need:

Let’s start with the HTML structure. We will create a parent container and give it an id of ‘hero’. Then we want to add multiple divs with class “layer” ( one for each layer you have created) and data-type attribute with value “parallax.

.layer{“data-type” => “parallax”}
.layer{“data-type” => “parallax”}
.layer{“data-type” => “parallax”}
.layer{“data-type” => “parallax”}
.layer{“data-type” => “parallax”}

Let’s add our basic styling. We’ll start by styling the id “hero”. I have set the height of the illustration to 800 px.

#hero {
height: 800px;
overflow: hidden;
position: relative;

Now, let’s move to styling the repeating class of layers. All of them will have the same height as the id “hero” container base positioning, and we’ll add position: fixed.

.layer {
background-position: bottom center;
background-size: auto;
background-repeat: no-repeat;
width: 100%;
height: 800px;
position: fixed;
z-index: -1;

The next thing we’ll want to do is add the layer images we have previously prepared. We’ll create another set of classes, one for every layer. Then, place the URL of the picture inside the “background-image” property.

.layer-01 {
background-image: url(‘input_link_to_image_here');
.layer-02 {
background-image: url(‘input_link_to_image_here');
// etc.

Let’s not forget to update the HTML file and assign the classes to the proper divs in our order, with the first layer being the background; the subsequent layers will be stacked on top of each other as you add the next numbers.

.layer.layer-01{“data-type” => “parallax”}
.layer.layer-02{“data-type” => “parallax”}
.layer.layer-03{“data-type” => “parallax”}
.layer.layer-04{“data-type” => “parallax”}
.layer.layer-05{“data-type” => “parallax”}

Javascript time

Now, let’s add a method that will check if the user has begun scrolling down.

window.addEventListener ‘scroll’, (event)

The EventTarget.addEventListener() method registers the specified listener on the EventTarget it’s called on. The event target may be an Element in a document, the Document itself, a Window, or any other object that supports events (such as XMLHttpRequest).

Then let’s store the value of pixels that the document has already been scrolled vertically into the topDistance variable. To do that, we’ll use the pageYOffset property.

window.addEventListener ‘scroll’, (event) ->
topDistance = @pageYOffset

After that, we want to select all the layers in our illustration and store them into a variable called ‘layers’. To do that, we’ll use the querySelectorAll method and data attribute inside the HTML that we have specified previously.

window.addEventListener ‘scroll’, (event) ->
topDistance = @pageYOffset
layers = document.querySelectorAll("[data-type='parallax']")

Next thing we’ll have to do is to loop through all the layers and apply proper transform to each of the layers. But before that, we need to specify one more thing inside of our HTML file, the data-depth. It will allow us to control how fast the elements move, let’s not dig too deep into the values inside of it yet, we’ll get back to this later on.

.layer.layer-01{“data-type” => “parallax”, "data-depth" => "0.10"}
.layer.layer-02{“data-type” => “parallax”, "data-depth" => "0.20"}
.layer.layer-03{“data-type” => “parallax”, "data-depth" => "0.50"}
.layer.layer-04{“data-type” => “parallax”, "data-depth" => "0.80"}
.layer.layer-05{“data-type” => “parallax”, "data-depth" => "1.00"}

For looping through all the elements, we’ll use the for loop. We start the loop by creating a variable where we will be storing our layers (coffescript allows us to do that all with a simple sentence for layer in layers); then take the value from the data-depth attribute we specified inside of our HTML. After that, we calculate the movement of the layers by multiplying the distance from the top of the page by our data-depth for the given layer. Element with the value of 1.0 will flow normally with the rest of the document, you can think of this as a “parallax off” state.

The last thing we do is we update the final value of movement to the layer’s CSS parameter of transform translate3d, to do that we’ll use style property along with all the prefixes for transform.

To make things more readable and DRY, we’ll store the translate3d property in a variable called ‘translate3d’.

for layer in layers
depth = layer.getAttribute(‘data-depth’)
movement = -(topDistance * depth)
translate3d = 'translate3d(0, ' + movement + 'px, 0)'['-webkit-transform'] = translate3d['-moz-transform'] = translate3d['-ms-transform'] = translate3d['-o-transform'] = translate3d = translate3d

Now when we specify the data-depth=”1.00" the element will move with the page as a standard element with no parallax effect. All values that are less than 100 will have the parallax effect.


For mobile we’ll be turning off the parallax version and replacing it with static image to save up on performance and file size (since it requires multiple png files). To do that we’ll create a new div below id hero with id of hero-mobile and apply display: none along with the background and height property.

#hero-mobile {
display: none;
background: url(“") no-repeat center bottom / cover;
height: 320px;

To show it instead of parallax we’ll use simple media query and apply display: none to desktop version, while overriding our display: none with display: block on hero-mobile.

@media all and (max-width: 640px) {
#hero {
display: none;
#hero-mobile {
display: block;

Further tweaking involves adding background-position to the individual layer classes to position elements as you wish.

Preview final code

Thank you so much for going through this guide. I sincerely hope that I have managed to show you a trick or two. I did learn a lot by writing this first tutorial of mine, and I would love to hear your feedback.

Thank you for hitting the if you liked the guide.
This will tell me to write more!

If you would like to see more of my work, check it out at: /




Head of Design, UX @ Jerry

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Data Structure: Bubble Sort

How To Add Javascript to WordPress Safely

Introducing StateLake: You’ll Love Managing States This Way

JavaScript — remove duplicated lines

Creating an Expressjs app using Ejs, Express-session with Redis

Some JavaScript/React Handy Helper Methods

Build Testing Rules for Your Design System

白 (Japanese Kanji) — white

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Patrick Westwood

Patrick Westwood

Head of Design, UX @ Jerry

More from Medium

How to use Tailwind CSS in Next JS

Nextjs/Reactjs: How to create Templates— The Pixels

Color Scheme and Dark/Light Mode Theming in React 17.x using CSS Only — Part 1

Mantine a clean React UI library