Scroll animation using the JavaScript build in Intersection Observer API (No third party libraries)

Adel Benyahia
7 min readDec 27, 2022
Photo by Marc-Olivier Jodoin on Unsplash

In this tutorial we will discus how to animate page sections (cards in our case) on our website using the new build-in Web Animations API, no extra package is needed

  1. What is Intersection Observer API
  2. Intersection Observer API browsers support
  3. Let’s code
  4. Horizontal delayed animation
  5. Complete project source code

1. What is Intersection Observer API

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport.

Historically, detecting visibility of an element, or the relative visibility of two elements in relation to each other, has been a difficult task for which solutions have been unreliable and prone to causing the browser and the sites the user is accessing to become sluggish. As the web has matured, the need for this kind of information has grown. Intersection information is needed for many reasons, such as:

Lazy-loading of images or other content as a page is scrolled.

Implementing “infinite scrolling” web sites, where more and more content is loaded and rendered as you scroll, so that the user doesn’t have to flip through pages.

Reporting of visibility of advertisements in order to calculate ad revenues.

Deciding whether or not to perform tasks or animation processes based on whether or not the user will see the result.

for more information: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API

2. Intersection Observer API browsers support

As you can see, Intersection Observer API is supported by all major browser

3. Let’s code

What are we building here?

Let’s start coding

We can call Intersection Oberrver API like this

const observer = new IntersectionObserver(callback, options);
  • callback function

callback is called when either of these circumstances occur:

A target element intersects either the device’s viewport or a specified element. That specified element is called the root element or root for the purposes of the Intersection Observer API.

The first time the observer is initially asked to watch a target element.

  • options object

{root, rootMargin, threshold}

root

The element that is used as the viewport for checking visibility of the target. Must be the ancestor of the target. Defaults to the browser viewport if not specified or if null.

rootMargin

Margin around the root. Can have values similar to the CSS margin property, e.g. "10px 20px 30px 40px" (top, right, bottom, left). The values can be percentages. This set of values serves to grow or shrink each side of the root element's bounding box before computing intersections. Defaults to all zeros.

threshold

Either a single number or an array of numbers which indicate at what percentage of the target’s visibility the observer’s callback should be executed. If you only want to detect when visibility passes the 50% mark, you can use a value of 0.5. If you want the callback to run every time visibility passes another 25%, you would specify the array [0, 0.25, 0.5, 0.75, 1]. The default is 0 (meaning as soon as even one pixel is visible, the callback will be run). A value of 1.0 means that the threshold isn’t considered passed until every pixel is visible.

In our project we have three files

. Styles.css

.card__container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.card {
width: 400px;
height: 150px;
border-radius: 5px;
margin: 5px;
padding: 5px;
box-shadow: 1px 4px 5px 0.5px #ccc;
color: gray;
}
.button {
height: 40px;
}
.hidden {
opacity: 0;
transform: translateX(-100%);
filter: blur(5px);
transition: all 1s;
}
@media (prefers-reduced-motion) {
.hidden {
transition: none;
}
}
.show {
opacity: 1;
transform: translateX(0);
filter: blur(0);
}

“card”, “card__container” and “button”, are basic CSS class to style card elements and buttons respectively.

The most important class is “show” and “hidden”

.hidden {
opacity: 0;
transform: translateX(-100%);
filter: blur(5px);
transition: all 1s;
}

The element to animate (card in our case) start with the class hidden

with opacity of 0, position “-100%” with translateX and blur 5px, when the element enter the view-port, the callback function will add the “show” CSS class to that element.

.show {
opacity: 1;
transform: translateX(0);
filter: blur(0);
}

Opacity of 1, position to 0, and blur 0

All that with an “ease-out” transition of 1 second

  transition: all 1s ease-in-out;

The prefers-reduced-motion CSS media feature is used to detect if the user has requested that the system minimize the amount of non-essential motion it uses.

@media (prefers-reduced-motion) {
.hidden {
transition: none;
}
}

. Index.html

<!DOCTYPE html>
<html>
<head>
<title>Intersection Observer API example</title>
<meta charset="UTF-8" />
<link rel="stylesheet" href="src/styles.css" />
</head>

<body>
<div id="app">
<h1>Intersection Observer API example</h1>
<p>
In this example we will demonstrate how to use the new build-in
Intersection Observer API
</p>
</div>
<div class="card__container">
<div id="card" class="card hidden">
<h1>Card1</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam...
</p>
</div>
<div id="card" class="card hidden">
<h1>Card2</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam...
</p>
</div>
<div id="card" class="card hidden">
<h1>Card3</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam...
</p>
</div>
<div id="card" class="card hidden">
<h1>Card4</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam...
</p>
</div>
<div id="card" class="card hidden">
<h1>Card5</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam...
</p>
</div>
<div id="card" class="card hidden">
<h1>Card6</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam...
</p>
</div>
<div id="card" class="card hidden">
<h1>Card7</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam...
</p>
</div>
<div id="card" class="card hidden">
<h1>Card8</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam...
</p>
</div>
<script defer src="src/index.js"></script>
</body>
</html>

Add a script tag to the end of the file, be sure to add “defer” option.

that will ensure that the javascript file will not executed until the document is fully loaded

<script defer src="src/index.js"></script>

As you can see the element “card” start with the class “hidden”

<div id="card" class="card hidden">
<h1>Card1</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam...
</p>
</div>

. index.js

const hidden = document.querySelectorAll(".hidden");
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("show");
} else {
entry.target.classList.remove("show");
}
});
},
{
treshold: 0.5
// rootMargin:"-50px"
}
);
hidden.forEach((entry) => {
observer.observe(entry);
});

hidden const will contain all the element will the CSS class “hidden”

const hidden = document.querySelectorAll(".hidden");

Then we create a new instance of the “IntersectionObserver”

const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("show");
} else {
entry.target.classList.remove("show");
}
});
},
{
treshold: 0.5
// rootMargin:"-50px"
}
);
  1. The callback function
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("show");
} else {
entry.target.classList.remove("show");
}
});
}

For All entries , if the element enter the view-port (entry.isIntersecting is true), We add the CSS class “show”

Else we remove it from this element.

2. For options

{
treshold: 0.5
// rootMargin:"-50px"
}

when treshold is 1, the element is animated when the all the element enter the view-port.

In our case 0.5, that means the animation begun when half of the element only enter to the view-port

With RootMargin: “-50” this means than the animation start 50px after the start, and 50px before the and of the root element.

You can use this option for example to start loading an image before it enter the view-port (lazy loading) with the value 100px

  {
...
// rootMargin:"100px"
}

4. Horizontal delayed animation

We will add another case here

We have 4 horizontal element, we can add a delay to show a smooth horizontal animation

Add this part to “styles.css”

.squares {
display: flex;
align-items: center;
margin: 10px;
margin-top: 400px;
}

.square {
width: 100px;
height: 100px;
margin: 2px;
background-color: #ccc;
text-align: center;
border: 2px solid #ccc;
font-size: 30px;
border-radius: 10px;
}
.square:nth-child(2) {
transition-delay: 100ms;
}
.square:nth-child(3) {
transition-delay: 220ms;
}
.square:nth-child(4) {
transition-delay: 360ms;
}

As you can see we added a “transition-delay property for each square child

.square:nth-child(2) {
transition-delay: 100ms;
}
.square:nth-child(3) {
transition-delay: 220ms;
}
.square:nth-child(4) {
transition-delay: 360ms;
}

Add this part of code the the “index.html” files

 <div id="card" class="squares">
<div class="square hidden"><p>1</p></div>
<div class="square hidden"><p>2</p></div>
<div class="square hidden"><p>3</p></div>
<div class="square hidden"><p>4</p></div>
</div>

5. Complete project source code

--

--

Adel Benyahia

Web application developer (HTML │ CSS │ JS | ReactJS | NextJS | NestJS | MERN)