Search code examples
javascripthtmlcssanimationeasing

css animation using keyframe not working as expected


I have come accross this problem and don't understand the behaviour.

Here we have a simple rectangle that should animate in and out on the press of the toggle button.

When the button is clicked, the rectangle class name will be toggled between 'animation-in' and 'animation-out'.

The keyframe object for 'scale-easeInOutBounce' and 'scale-easeInOutBounce-out' is identical, only the name reference is different between the two.

Everything works perfectly.

When the property 'animation-name:' within the css rule'.animation-out' is changed from 'scale-easeInOutBounce-out' to 'scale-easeInOutBounce' the animation breaks.

The question here is, why does this occur? Is there a logic error in the code, or an incorrect implementation of animation?

Would love to understand this behaviour.

This behaviour was replicated using 'Chrome' on Windows 11


<!DOCTYPE html>
<html>
<head>
    <title>Rectangle Animation</title>
    <style>
        #myRectangle {
            width: 100px;
            height: 150px;
            background-color: green;
            margin: 20px;
            transform: scale(0);
            
        }

        .animation-out {
            animation-duration: 4s;
            animation-timing-function: ease; /*linear*/
            animation-delay: 0s;
            animation-iteration-count: 1;
            animation-direction: normal;
            animation-fill-mode: forwards;
            animation-play-state: running;
            animation-name: scale-easeInOutBounce;
            animation-timeline: auto;
            animation-range-start: normal;
            animation-range-end: normal;
        }

        .animation-in {
            animation-duration: 4s;
            animation-timing-function: ease;
            animation-delay: 0s;
            animation-iteration-count: 1;
            animation-direction: reverse;
            animation-fill-mode: forwards;
            animation-play-state: running;
            animation-name: scale-easeInOutBounce;
            animation-timeline: auto;
            animation-range-start: normal;
            animation-range-end: normal;
        }

        @keyframes scale-easeInOutBounce {
            0% { transform: scale(1); }
            2% { transform: scale(0.99); }
            4% { transform: scale(1); }
            10% { transform: scale(0.97); }
            14% { transform: scale(0.99); }
            22% { transform: scale(0.88); }
            32% { transform: scale(0.99); }
            42% { transform: scale(0.6); }
            50% { transform: scale(0.5); }
            58% { transform: scale(0.4); }
            68% { transform: scale(0.01); }
            78% { transform: scale(0.12); }
            86% { transform: scale(0.01); }
            90% { transform: scale(0.03); }
            96% { transform: scale(0); }
            98% { transform: scale(0.01); }
            100% { transform: scale(0); }
        }

        @keyframes scale-easeInOutBounce-out {
            0% { transform: scale(1); }
            2% { transform: scale(0.99); }
            4% { transform: scale(1); }
            10% { transform: scale(0.97); }
            14% { transform: scale(0.99); }
            22% { transform: scale(0.88); }
            32% { transform: scale(0.99); }
            42% { transform: scale(0.6); }
            50% { transform: scale(0.5); }
            58% { transform: scale(0.4); }
            68% { transform: scale(0.01); }
            78% { transform: scale(0.12); }
            86% { transform: scale(0.01); }
            90% { transform: scale(0.03); }
            96% { transform: scale(0); }
            98% { transform: scale(0.01); }
            100% { transform: scale(0); }
        }
    </style>
</head>
<body>

<div id="myRectangle"></div>
<button id="toggleButton">Toggle Animation</button>

<script>
    document.getElementById('toggleButton').addEventListener('click', function() {
        var rectangle = document.getElementById('myRectangle');
        if (rectangle.classList.contains('animation-in')) {
            rectangle.classList.remove('animation-in');
            rectangle.classList.add('animation-out');
        } else {
            rectangle.classList.remove('animation-out');
            rectangle.classList.add('animation-in');
        }
    });
</script>

</body>
</html>

https://jsfiddle.net/midnightstudios/vajqdcLo/2/

Here is the code that is broken:

<!DOCTYPE html>
<html>
<head>
    <title>Rectangle Animation</title>
    <style>
        #myRectangle {
            width: 100px;
            height: 150px;
            background-color: green;
            margin: 20px;
            transform: scale(0);
            
        }

        .animation-out {
            animation-duration: 4s;
            animation-timing-function: ease; /*linear*/
            animation-delay: 0s;
            animation-iteration-count: 1;
            animation-direction: normal;
            animation-fill-mode: forwards;
            animation-play-state: running;
            animation-name: scale-easeInOutBounce;
            animation-timeline: auto;
            animation-range-start: normal;
            animation-range-end: normal;
        }

        .animation-in {
            animation-duration: 4s;
            animation-timing-function: ease;
            animation-delay: 0s;
            animation-iteration-count: 1;
            animation-direction: reverse;
            animation-fill-mode: forwards;
            animation-play-state: running;
            animation-name: scale-easeInOutBounce;
            animation-timeline: auto;
            animation-range-start: normal;
            animation-range-end: normal;
        }

        @keyframes scale-easeInOutBounce {
            0% { transform: scale(1); }
            2% { transform: scale(0.99); }
            4% { transform: scale(1); }
            10% { transform: scale(0.97); }
            14% { transform: scale(0.99); }
            22% { transform: scale(0.88); }
            32% { transform: scale(0.99); }
            42% { transform: scale(0.6); }
            50% { transform: scale(0.5); }
            58% { transform: scale(0.4); }
            68% { transform: scale(0.01); }
            78% { transform: scale(0.12); }
            86% { transform: scale(0.01); }
            90% { transform: scale(0.03); }
            96% { transform: scale(0); }
            98% { transform: scale(0.01); }
            100% { transform: scale(0); }
        }
    </style>
</head>
<body>

<div id="myRectangle"></div>
<button id="toggleButton">Toggle Animation</button>

<script>
    document.getElementById('toggleButton').addEventListener('click', function() {
        var rectangle = document.getElementById('myRectangle');
        if (rectangle.classList.contains('animation-in')) {
            rectangle.classList.remove('animation-in');
            rectangle.classList.add('animation-out');
        } else {
            rectangle.classList.remove('animation-out');
            rectangle.classList.add('animation-in');
        }
    });
</script>

</body>
</html>


Solution

  • I can explain the cause of the behavior and what you can do to fix it.

    Cause

    The reason the outward animation appears to break is because it actually seems to start in the middle of it. More specifically—if you hit the button, the inward animation will play for 4 seconds, right? Then let's say you wait 8 seconds before hitting the button a second time. For some reason, the outward animation will start 12 seconds into its animation. As proof, try setting the duration of the outward animation to 20 seconds and try hitting the buttons again. You'll notice that the outward animation starts partway in.

    Solution

    If you wanted to keep using the same animation name and keep your code the way it is, one solution is actually to add a delay before you add the classes. I edited the example and showed it below. You could for example use something like setTimeout(() => rectangle.classList.add('animation-out'), 0), but a better function to use would be requestAnimationFrame() because it minimizes the delay.

    document.getElementById('toggleButton').addEventListener('click', function() {
            var rectangle = document.getElementById('myRectangle');
            if (rectangle.classList.contains('animation-in')) {
                rectangle.classList.remove('animation-in');
                requestAnimationFrame(() => {rectangle.classList.add('animation-out');})
                
            } else {
                rectangle.classList.remove('animation-out');
                requestAnimationFrame(() => {rectangle.classList.add('animation-in');})
            }
        });
    #myRectangle {
                width: 100px;
                height: 150px;
                background-color: green;
                margin: 20px;
                transform: scale(0);
            }
    
    .animation-out {
      animation-duration: 4s;
      animation-timing-function: ease;
      animation-direction: normal;
      animation-fill-mode: forwards;
      animation-name: scale-easeInOutBounce;
    }
    
    .animation-in {
      animation-duration: 4s;
      animation-timing-function: ease;
      animation-direction: reverse;
      animation-fill-mode: forwards;
      animation-name: scale-easeInOutBounce;
    }
    
    @keyframes scale-easeInOutBounce {
      0% { transform: scale(1); }
      2% { transform: scale(0.99); }
      4% { transform: scale(1); }
      10% { transform: scale(0.97); }
      14% { transform: scale(0.99); }
      22% { transform: scale(0.88); }
      32% { transform: scale(0.99); }
      42% { transform: scale(0.6); }
      50% { transform: scale(0.5); }
      58% { transform: scale(0.4); }
      68% { transform: scale(0.01); }
      78% { transform: scale(0.12); }
      86% { transform: scale(0.01); }
      90% { transform: scale(0.03); }
      96% { transform: scale(0); }
      98% { transform: scale(0.01); }
      100% { transform: scale(0); }
    }
    
    @keyframes scale-easeInOutBounce-out {
      0% { transform: scale(1); }
      2% { transform: scale(0.99); }
      4% { transform: scale(1); }
      10% { transform: scale(0.97); }
      14% { transform: scale(0.99); }
      22% { transform: scale(0.88); }
      32% { transform: scale(0.99); }
      42% { transform: scale(0.6); }
      50% { transform: scale(0.5); }
      58% { transform: scale(0.4); }
      68% { transform: scale(0.01); }
      78% { transform: scale(0.12); }
      86% { transform: scale(0.01); }
      90% { transform: scale(0.03); }
      96% { transform: scale(0); }
      98% { transform: scale(0.01); }
      100% { transform: scale(0); }
    }
    <!DOCTYPE html>
    <html>
    <head>
        <title>Rectangle Animation</title>
    </head>
    <body>
    
    <div id="myRectangle"></div>
    <button id="toggleButton">Toggle Animation</button>
    </body>
    </html>

    Why the behavior occurs in the first place? I do not know for sure. In general, you can expect some pretty weird behavior when it comes to swapping out keyframes in various circumstances (whether it involves pure CSS or the JavaScript WAAPI)—I can speak from personal experience on that. In this case, maybe swapping out the classes immediately makes it get confused and think that it's just swapping out the keyframes for a singular animation instead of swapping out the animation entirely, so it erroneously keeps the duration going instead of starting from 0?