Create a Parallax Scrolling Effect (With Contrasting Text Effect)
Time to refresh our parallax knowledge with a new exercise, folks! In today’s tutorial, we’re going to create a beautiful parallax scrolling animation and contrast the heading text against the image behind it.
What We’re Going to Build
Here’s our parallax demo page—scroll down to see how the text contrasts to the background by changing color!
1. Begin With the HTML Markup
The markup will consist of two sections:
- The first hero section will visually include only the heading and the profile image. But in reality, we’ll put a heading and a wrapper element that will enclose the image and a heading clone/duplicate. By saying clone, we mean its content and appearance will be identical to the target element.
- The second section will include the page’s main content. Of course, you can have many more sections on your own pages.
Here’s the required markup:
1 |
<section class="section section-hero"> |
2 |
<h1 class="h1 hero-title">Visual <br> Designer</h1> |
3 |
<div class="hero-img-wrapper"> |
4 |
<img width="1308" height="1220" src="man-visual-designer.jpg" alt=""> |
5 |
<div class="h1 hero-title-clone"> |
6 |
Visual <br> Designer
|
7 |
</div>
|
8 |
</div>
|
9 |
</section>
|
10 |
<section class="section section-text"> |
11 |
<div>...</div> |
12 |
</section>
|
2. Add the CSS
Let’s now concentrate on the key styles.
We’ll start with those targets in the hero section.



- The heading will be white while the clone salmon.
- The clone will be absolutely positioned inside the image’s wrapper element. Its
top
andleft
position values will be set through JavaScript; the end goal is to place it on top of the heading at the exact same position. More on that in the next section. To make that happen, we should ensure that its stacking order is higher than the heading’s one. In our case, this occurs as the wrapper comes after the heading. - The image’s wrapper element will have
overflow: hidden
so the clone cannot exceed its boundaries.
The associated styles:
1 |
/*CUSTOM VARIABLES HERE*/
|
2 |
|
3 |
.section-hero { |
4 |
display: flex; |
5 |
flex-direction: column; |
6 |
align-items: center; |
7 |
justify-content: center; |
8 |
max-width: 1000px; |
9 |
margin: 0 auto; |
10 |
}
|
11 |
|
12 |
.section-hero .h1 { |
13 |
font-size: 20vmin; |
14 |
line-height: 0.9; |
15 |
font-weight: bold; |
16 |
text-align: center; |
17 |
text-shadow: 2px 2px 10px #1a1423; |
18 |
}
|
19 |
|
20 |
.section-hero .hero-title-clone { |
21 |
position: absolute; |
22 |
color: var(--salmon); |
23 |
}
|
24 |
|
25 |
.section-hero .hero-img-wrapper { |
26 |
position: relative; |
27 |
margin-top: -10vh; |
28 |
overflow: hidden; |
29 |
}
|
30 |
|
31 |
.section-hero .hero-img-wrapper img { |
32 |
border-radius: 10px; |
33 |
}
|
Regarding the main content styles, an important aspect is that all sibling sections after the hero one should have a higher z-index
value. That way, the scrolling heading will always be below those sections.
In our case, all we have below is a fixed background image with text on top of it. To ensure that the text will be readable, we’ll put an overlay image.



The relevant styles:
1 |
.section-hero ~ .section { |
2 |
position: relative; |
3 |
z-index: 2; |
4 |
}
|
5 |
|
6 |
.section-text { |
7 |
background: url(man-walking.jpg) no-repeat fixed center right / cover; |
8 |
margin-top: 20vh; |
9 |
}
|
10 |
|
11 |
.section-text::before { |
12 |
content: ""; |
13 |
position: absolute; |
14 |
top: 0; |
15 |
left: 0; |
16 |
right: 0; |
17 |
bottom: 0; |
18 |
background: rgba(56, 36, 52, 0.5); |
19 |
}
|
20 |
|
21 |
.section-text div { |
22 |
position: relative; |
23 |
max-width: 700px; |
24 |
padding: 100px 0; |
25 |
margin: 0 auto; |
26 |
}
|
27 |
|
28 |
.section-text p { |
29 |
margin-top: 20px; |
30 |
}
|
3. Apply the JavaScript
The first thing we have to do is to place the clone in the heading’s place like this:



That should happen when all the page assets have been loaded and during the window resize. In your projects, to avoid having this jump until the clone gets the correct position, you can add a preloader or something like that.
At that point, we’ll call the setPos()
function and follow this plan:
- Grab the
x
andy
positions of the heading relative to the viewport. - Grab the
x
andy
positions of the image’s wrapper element relative to the viewport. - Set the
top
andleft
property values of the clone by subtracting the heading’s values from the wrapper’s values. Especially to prevent the disposition of our clone as we suddenly resize the window height, we also have to subtract from the finaltop
value the amount of our scrolling (we explain how to calculate this in the next paragraph).
To create the parallax effect, as we scroll, we’ll use the transform
property to move the heading and its clone at the same time and direction so they’ll look like a single element. The speed movement will be determined by the getSpeed()
function. Our base speed will be 1.1 which means the target elements will move 10% faster than the other elements. Change this value to see the difference in the effect.
With all these in mind, here’s all the JavaScript code we’ll need:
1 |
const sectionHero = document.querySelector(".section-hero"); |
2 |
const heroTitle = sectionHero.querySelector(".hero-title"); |
3 |
const heroTitleClone = sectionHero.querySelector(".hero-title-clone"); |
4 |
const heroImgWrapper = sectionHero.querySelector(".hero-img-wrapper"); |
5 |
const speed = 1.1; |
6 |
|
7 |
window.addEventListener("load", setPos); |
8 |
window.addEventListener("resize", setPos); |
9 |
|
10 |
function setPos() { |
11 |
const { x: heroTitleX, y: heroTitleY } = heroTitle.getBoundingClientRect(); |
12 |
const { |
13 |
x: heroImgWrapperX, |
14 |
y: heroImgWrapperY |
15 |
} = heroImgWrapper.getBoundingClientRect(); |
16 |
|
17 |
heroTitleClone.style.top = `${heroTitleY - getSpeed() - heroImgWrapperY}px`; |
18 |
heroTitleClone.style.left = `${heroTitleX - heroImgWrapperX}px`; |
19 |
}
|
20 |
|
21 |
window.addEventListener("scroll", function () { |
22 |
const speed = `translateY(${getSpeed()}px)`; |
23 |
heroTitle.style.transform = speed; |
24 |
heroTitleClone.style.transform = speed; |
25 |
});
|
26 |
|
27 |
function getSpeed() { |
28 |
return window.pageYOffset * speed; |
29 |
}
|
Conclusion
With less than 30 lines of JavaScript code, we managed to build this lovely parallax effect that is a perfect scenario for a portfolio website or whenever you want to create a contrast between the text and its background. Hopefully, you’re convinced enough to embed it in one of your projects.
Before closing, let’s look again at what we created today:
As always, thanks a lot for reading!