Skip to content
Advertisement

Problem with using IntersectionObserver to trigger CSS animation

I am trying to use IntersectionObserver to observe the 3 container so that the wipe-enter animation will start when they are inside the viewport one by one.

If I scroll to a container (that is outside the viewport) slowly so that only part of it is inside the viewport, the container keeps flickering until it is fully inside the viewport.

I tried to inspect the container when it is flickering and it seems that the container-animation class is being added and removed constantly until the container is fully inside the viewport.

This is the first time I use IntersectionObserver so I am not sure how the code should be changed to stop them from flickering.

Any help will be appreciated. Thank you.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Animation</title>
    <style>
        .container {
            width: 300px;
            height: 300px;
            background: green;
            margin: 500px auto;
        }

        @keyframes wipe-enter {
            0% { transform: scale(0, .025); }
            50% { transform: scale(1, .025); }
        }

        .container-animation {
            animation: wipe-enter 1s 1;
        }
    </style>
</head>

<body>
    <div class="container"></div>    
    <div class="container"></div>    
    <div class="container"></div>    
</body>

<script>
    // Register IntersectionObserver
    const io = new IntersectionObserver(entries => {
    entries.forEach(entry => {
        // Add 'container-animation' class if observation target is inside viewport
        if (entry.intersectionRatio > 0) {
            entry.target.classList.add('container-animation');
        }
        else {
            // Remove 'container-animation' class
            entry.target.classList.remove('container-animation');
        }
    })
    })

    // Declares what to observe, and observes its properties.
    const containers = document.querySelectorAll('.container');
    containers.forEach((el) => {
        io.observe(el);
    })
</script>
</html>

Advertisement

Answer

When an element is scaled it still ‘takes up’ the same space in the page – in the sense that other items are not affected. However, scaling takes place by default from the central point of the element.

So, when an element gets into the viewport your code immediately scales it right down and then gradually increases its height but from the center which will at that time be nearly 150px below (or above) the viewport bottom/top.

So you get told it’s gone out of the viewport and you remove the animation. The element goes back to 300px high and so enters the viewport and so on. Hence ‘flashing’.

One way to prevent this is to not remove the animation when the item goes out of the viewport but when the animation has finished – then it doesn’t matter that it’s shrunk through the scaling and isn’t in the viewport for part of a second.

But, in order to prevent other elements moving we can’t just do this by changing the height of the element, that needs to remain constant. This code scales a before pseudo element on each of the containers.

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Animation</title>
    <style>
        .container {
            width: 300px;
            height: 300px;
            margin: 500px auto;
            position: relative;
        }

        @keyframes wipe-enter {
            0% { transform: scale(0, .025); }
            50% { transform: scale(1, .025); }
            100% { transform: scale(1, 1); }
        }
        .container::before {
          content: '';
          position: absolute;
          width: 100%;
          height: 100%;
          left: 0;
          top: 0;
          background: green;
        }

        .container.container-animation::before {
            animation: wipe-enter 1s 1;
        }
    </style>
</head>

<body>
    <div class="container"></div>    
    <div class="container"></div>    
    <div class="container"></div>    
</body>

<script>
    // Register IntersectionObserver
    const io = new IntersectionObserver(entries => {
    entries.forEach(entry => {
        // Add 'container-animation' class if observation target is inside viewport
        if (entry.intersectionRatio > 0) {
            entry.target.classList.add('container-animation');
        }
    })
    })

    // Declares what to observe, and observes its properties.
    const containers = document.querySelectorAll('.container');
    containers.forEach((el) => {
        io.observe(el);
        el.addEventListener('animationend', function () {
          el.classList.remove('container-animation');
        });
    })
</script>
</html>
Advertisement