Search code examples
csscss-animationsdelay

css animations end together even though they are started at different times


I have a few elements of the same type and I want them to share the same css animation, but I want them to start/end the animation at different times.

Codepen for the following code

The html:

<div class="container">
    <div class="box hidden"></div>
</div>
<div class="container">
    <div class="box hidden"></div>
</div>
<div class="container">
    <div class="box hidden"></div>
</div>

The css:

.container {
    width: 100px;
    height: 100px;
    margin-bottom: 20px;
}

.box {
    width: 100%;
    height: 100%;
}

.box.hidden {
    visibility: hidden;
}

.box {
    animation: growIn 1s;
    animation-timing-function: cubic-bezier(.46,.13,.99,.83);
    transition: all .2s cubic-bezier(0.215, 0.61, 0.355, 1);
}

.container:first-child .box {
    background-color: green;
}

.container:nth-child(2) .box {
    background-color: orange;
}

.container:nth-child(3) .box {
    background-color: red;
}

@keyframes growIn {
    from {
        transform: scale(0);
    }
    to {
        transform: scale(1);
    }
}

The box elements start as hidden, and then using javascript I remove this classname from the different boxes but at different times:

const boxes = document.querySelectorAll(".box");
boxes.forEach(box => {
    setTimeout(() => box.classList.remove("hidden"), Math.random() * 1000);
});

What happens is that all 3 boxes end their animation at the exact same time. The animation does start at different times, but all end together.

Why is that?
If I do the same but add a classname instead of removing it (in order to make the animation start) then it behaves just as I want it to.
Any ideas? Thanks.


Solution

  • Simply because all the animation have already started at the same time. Using visibility:hidden will not prevent the animation to start and make it start later when the element is visible. The same thing will happen with opacity for example:

    const boxes = document.querySelectorAll(".box");
    boxes.forEach(box => {
      setTimeout(() => box.classList.remove("hidden"), Math.random() * 5000);
    });
    .container {
      width: 100px;
      height: 100px;
      margin-bottom: 20px;
    }
    
    .box {
      width: 100%;
      height: 100%;
    }
    
    .box.hidden {
      opacity: 0.1;
    }
    
    .box {
      animation: growIn 5s;
      animation-timing-function: cubic-bezier(.46, .13, .99, .83);
      transition: all .2s cubic-bezier(0.215, 0.61, 0.355, 1);
    }
    
    .container:first-child .box {
      background-color: green;
    }
    
    .container:nth-child(2) .box {
      background-color: orange;
    }
    
    .container:nth-child(3) .box {
      background-color: red;
    }
    
    @keyframes growIn {
      from {
        transform: scale(0);
      }
      to {
        transform: scale(1);
      }
    }
    <div class="container">
      <div class="box hidden"></div>
    </div>
    <div class="container">
      <div class="box hidden"></div>
    </div>
    <div class="container">
      <div class="box hidden"></div>
    </div>

    You can see the behavior you are looking for if you use the display property instead:

    const boxes = document.querySelectorAll(".box");
    boxes.forEach(box => {
      setTimeout(() => box.classList.remove("hidden"), Math.random() * 3000);
    });
    .container {
      width: 100px;
      height: 100px;
      margin-bottom: 20px;
    }
    
    .box {
      width: 100%;
      height: 100%;
    }
    
    .box.hidden {
      display:none;
    }
    
    .box {
      animation: growIn 1s;
      animation-timing-function: cubic-bezier(.46, .13, .99, .83);
      transition: all .2s cubic-bezier(0.215, 0.61, 0.355, 1);
    }
    
    .container:first-child .box {
      background-color: green;
    }
    
    .container:nth-child(2) .box {
      background-color: orange;
    }
    
    .container:nth-child(3) .box {
      background-color: red;
    }
    
    @keyframes growIn {
      from {
        transform: scale(0);
      }
      to {
        transform: scale(1);
      }
    }
    <div class="container">
      <div class="box hidden"></div>
    </div>
    <div class="container">
      <div class="box hidden"></div>
    </div>
    <div class="container">
      <div class="box hidden"></div>
    </div>


    From the specification:

    The 'visibility' property specifies whether the boxes generated by an element are rendered.

    Invisible boxes still affect layout (set the 'display' property to 'none' to suppress box generation altogether).

    So the box is always generated when using visibility unlike when using display.

    And if we check the specification related to the animation we will find this:

    Setting the display property to none will terminate any running animation applied to the element and its descendants. If an element has a display of none, updating display to a value other than none will start all animations applied to the element by the animation-name property, as well as all animations applied to descendants with display other than none.