Note10 minute read

Animating elements as they enter and leave the screen with JavaScript

How to use viewport position to coordinate enter and exit animations with JavaScript.

  • frontend
  • javascript
  • animation

How do you test if an element is in the viewport?

There are many ways to do this in JavaScript. It's useful for animating elements as they become visible when they enter the viewport, improving the experience and immersion of your app.

In this tutorial, I won't focus on the animations themselves. That's too personal, both to the developer and the project.

The idea is to show a simple alternative you can implement quickly, so you can capture an element's position and animate it on entry or exit from the window.


We start with the basic structure (index.html). We'll use a set of 6 random images from the Unsplash API. These images get animated in two situations: when they "leave" above or below the visible area of the window, the viewport.

<!DOCTYPE html>
<html lang="pt-br">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="style.css">
  <title>Document</title>
</head>
<body>
  <img src="https://source.unsplash.com/random" class="image" alt="">
  <img src="https://source.unsplash.com/random" class="image" alt="">
  <img src="https://source.unsplash.com/random" class="image" alt="">
  <img src="https://source.unsplash.com/random" class="image" alt="">
  <img src="https://source.unsplash.com/random" class="image" alt="">
  <img src="https://source.unsplash.com/random" class="image" alt="">
  <script src="script.js"></script>
</body>
</html>

Next, we add demo styles in style.css for the body and images:

body {
  padding: 10rem 5rem;
  
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 10rem;

  background: #121212;
  overflow-x: hidden;
}

img {
  width: 100%;
  max-width: 600px;
  height: 400px;
  object-fit: cover;
  
  transition: 0.5s;
}

Finally, still in the styles, we create two classes for the two possible viewport exits:

  • .is-down, applied when the element is below the visible area
  • .is-up, applied when the element is above the visible area

The properties here are just for demonstration. Feel free to build your own transitions for the effect you want.

.is-down {
  transform: translateX(25%);
  opacity: 0;
}

.is-up {
  transform: translateX(-25%);
  opacity: 0;
}

Capture and animate

In script.js, we start by grabbing our image list with querySelectorAll, which returns every element with the image class:

const images = document.querySelectorAll(".image");

Next, we capture the window height. Since we want to animate images leaving above and below the visible area, knowing the viewport height is essential to tell whether an element is visible:

let windowHeight = window.innerHeight;

We create a function to animate the images. It uses forEach to loop through the list and apply the needed changes.

For each image, we create a variable called bounding and assign it the DOMRect object returned by getBoundingClientRect().

That object has the element's dimensions and its coordinates relative to the viewport. The code below shows an example of its structure. It won't be part of our example.

Property values are in pixels.

{
  bottom: -413.316650390625,
​  height: 400,
​  left: 491.5,
​  right: 1091.5,
​  top: -813.316650390625,
  width: 600,
​  x: 491.5,
​  y: -813.316650390625
}

From those coordinates, assigned to bounding, we can tell whether an element is inside the visible area:

The Y axis starts at the top of the page, so that position is 0. The bottom of the page equals the value stored in windowHeight.

If bounding.bottom, the bottom of the image, is greater than windowHeight, the image is not inside the viewport. It's below the visible area, fully or partially.

If bounding.top, the top of the image, is less than 0, the image is not inside the viewport. It's above the visible area, fully or partially.

From there, we apply the matching classes. If neither condition is true, we remove the classes so the image returns to its default appearance while visible.

function animateImages() {
  images.forEach((image) => {
    let bounding = image.getBoundingClientRect();
    if (bounding.bottom > windowHeight) {
      image.classList.add("is-down");
    } else if (bounding.top < 0) {
      image.classList.add("is-up");
    } else {
      image.classList.remove("is-up");
      image.classList.remove("is-down");
    }
  });
}

Since we want this effect during page scroll, we add a listener that captures scroll events and runs animateImages().

document.addEventListener("scroll", function () {
  animateImages();
  document.removeEventListener("scroll", this);
});

We also add a listener for window resize, updating windowHeight with the new value.

window.addEventListener("resize", function () {
  windowHeight = window.innerHeight;
  window.removeEventListener("resize", this);
});

So images that aren't visible get their classes on load, we run animateImages() as soon as the app starts.

animateImages();

You can see the demo here


You can explore other conditions with the DOMRect from getBoundingClientRect().

To require the element to leave the viewport completely before the transition: bounding.bottom < 0 (exited above) or bounding.top > windowHeight (exited below).

You can also add a margin, for example animating only when 10% of the screen remains until the end.