Stop infinite CSS3 animation and smoothly revert to initial state
Asked Answered
C

2

5

Having some trouble building a CSS3 loader using keyframe animations.

The loader consists of 4 boxes that animate going up and down. The issue I'm having is that when the animation is supposed to stop, the boxes jump to the initial position. The behaviour I'm looking for is: loader is animating infinitely until loading is done, at which point it should animate to the initial position and stop, sort of like having animation-iteration-count: infinite and changing it to animation-iteration-count: 1 to stop the animation. (which doesn't work btw).

See this fiddle to see what I mean: https://jsfiddle.net/cazacuvlad/qjmhm4ma/ (when clicking the stop button, the boxes should animate to the initial position, instead of jumping)

The basic setup is:

<div class="loader-wrapper"><span></span><span></span><span></span><span></span></div>

To start the loader, I'm adding a loader-active class that contains the animation to the loader-wrapper.

LESS:

.loader-wrapper {
  &.loader-active {
    span {
      .animation-name(loader);
      .animation-duration(1200ms);
      .animation-timing-function(ease-in-out);
      .animation-play-state(running);
      .animation-iteration-count(infinite);

      &:nth-child(1) {
      }
      &:nth-child(2) {
        .animation-delay(300ms);
      }
      &:nth-child(3) {
        .animation-delay(600ms);
      }
      &:nth-child(4) {
        .animation-delay(900ms);
      }
    }
  }
}

I've tried adding the animation to the spans in the loader-wrapper class w/o loader-active and playing around with animation-iteration-count and animation-play-state when loader-active is added without any luck.

Chassis answered 19/2, 2016 at 14:18 Comment(5)
You can't do that with CSS. You'd need what I suspect would be quick complex Javascript.Rescission
Doing it in JS is the backup solution. I think this is valid use case for animations, there should be a pure CSS way of doing this.Chassis
The animations are fine but CSS can't detect where an animation is in it's (mid)cycle and then run down based on a condition. I'm not even sure if JS can do that but if it can, that would be the way.Rescission
It might not be impossible but it would not be straight-forward. You can refer to this thread. Within that answer I have linked to another answer by vals which shows a method to achieve something like this. (Note: I linked to my answer instead of directly linking to vals' because there is some explanation in my answer which might also be helpful for you.)Hawking
I found a pretty simple workaround, not entirely happy with it since it still involves JS, but it works well. Posting it as an answer in case someone else stumbles on this.Chassis
C
8

Found a pretty simple workaround. Still not pure CSS, it involves a bit of JS, but it works well.

Updated fiddle: https://jsfiddle.net/cazacuvlad/qjmhm4ma/2/

What I did was to move the loader-active class to each span (instead of the wrapper), listen to the animationiteration event on each span and stop the animation then.

$('.loader-wrapper span').on('animationiteration webkitAnimationIteration', function () {
  var $this = $(this);

  $this.removeClass('loader-active');

  $this.off();
});

This basically stops the animation at the very end of an iteration cycle.

Updated LESS

.loader-wrapper {
  span {
    &.loader-active {
      .animation-name(loader);
      .animation-duration(1200ms);
      .animation-timing-function(ease-in-out);
      .animation-play-state(running);
      .animation-iteration-count(infinite);

      &:nth-child(1) {
      }
      &:nth-child(2) {
        .animation-delay(300ms);
      }
      &:nth-child(3) {
        .animation-delay(600ms);
      }
      &:nth-child(4) {
        .animation-delay(900ms);
      }
    }
  }
}
Chassis answered 19/2, 2016 at 15:12 Comment(3)
That is actually a very nice idea. I liked it :) The cases that I linked were more complex because those needed a gradual pause instead of completion of an iteration.Hawking
Unfortunately, this doesn't work if the animation-direction is alternate. We have to end it manually. Great solution though! A possible workaround could be to get the current animation-duration and compare it to the elapsedTime.Sarsaparilla
This was just what i needed!! FYI, as a workaround for animation-direction: alternate i just changed my animation to go back and forth via 0%, 50%, 100% instead of by alternating, works greatFreeforall
H
1

You can also add a class which specifies the iteration count to stop the infinite loop. The advantage of this approach is that you can change the duration and timing-function which can be nice for easing out some animation (Like a rotating logo for example).

.animate-end {
  animation-iteration-count: 3;
  animation-duration: 1s;
  animation-timing-function: ease-out;
}

We can add this class with js and it will now stop the animation at count 3.

document.querySelector(".loader-wrapper").classList.add("animate-end");

But you can also end the current itertion by counting it and change the style of the element dynamcly with Js.

let iterationCount = 0;
document.querySelector(".loader-wrapper span").addEventListener('animationiteration', () => {
//count iterations
  iterationCount++;
});    

yourElement.style.animationIterationCount = iterationCount + 1;

Here is a demo with your code:

document.querySelector("#start_loader").addEventListener("click", function(){
  document.querySelector(".loader-wrapper").classList.add("loader-active");  
})


let iterationCount = 0;
document.querySelector(".loader-wrapper span").addEventListener('animationiteration', () => {
//count iterations
  iterationCount++;
  console.log(`Animation iteration count: ${iterationCount}`);
});

document.querySelector("#stop_loader").addEventListener("click", function(){
    
    //For some animation it can be nice to change the duration or timing animation
    document.querySelector(".loader-wrapper").classList.add("animate-end");
    
    //End current iteration
     document.querySelectorAll(".loader-wrapper span").forEach(element => {
        element.style.animationIterationCount = iterationCount + 1;
    });


    //Remove Classes with a timeout or animationiteration event
    setTimeout(() => {
        document.querySelector(".loader-wrapper").classList.remove("loader-active");
         document.querySelector(".loader-wrapper").classList.remove("animate-end");
     }, 1200);
    

})
@-moz-keyframes 'loader' {
  0% {
    -moz-transform: translate3d(0, 0, 0);
  }
  50% {
    -moz-transform: translate3d(0, -10px, 0);
  }
  100% {
    -moz-transform: translate3d(0, 0, 0);
  }
}

@-webkit-keyframes 'loader' {
  0% {
    -webkit-transform: translate3d(0, 0, 0);
  }
  50% {
    -webkit-transform: translate3d(0, -10px, 0);
  }
  100% {
    -webkit-transform: translate3d(0, 0, 0);
  }
}

@-o-keyframes 'loader' {
  0% {
    -o-transform: translate3d(0, 0, 0);
  }
  50% {
    -o-transform: translate3d(0, -10px, 0);
  }
  100% {
    -o-transform: translate3d(0, 0, 0);
  }
}

@keyframes 'loader' {
  0% {
    transform: translate3d(0, 0, 0)
  }
  50% {
    transform: translate3d(0, -10px, 0)
  }
  100% {
    transform: translate3d(0, 0, 0)
  }
}
.loader-wrapper {
  margin-bottom: 30px;
}


.loader-wrapper.loader-active span {
  -webkit-animation-name: loader;
  -moz-animation-name: loader;
  -ms-animation-name: loader;
  -o-animation-name: loader;
  animation-name: loader;
  -webkit-animation-duration: 1200ms;
  -moz-animation-duration: 1200ms;
  -ms-animation-duration: 1200ms;
  -o-animation-duration: 1200ms;
  -webkit-animation-timing-function: ease-in-out;
  -moz-animation-timing-function: ease-in-out;
  -ms-animation-timing-function: ease-in-out;
  -o-animation-timing-function: ease-in-out;
  animation-timing-function: ease-in-out;
  -webkit-animation-play-state: running;
  -moz-animation-play-state: running;
  -ms-animation-play-state: running;
  -o-animation-play-state: running;
  animation-play-state: running;
  -webkit-animation-iteration-count: infinite;
  -moz-animation-iteration-count: infinite;
  -ms-animation-iteration-count: infinite;
  -o-animation-iteration-count: infinite;
  animation-iteration-count: infinite;
}


.loader-wrapper.animate-end span {
    /* Works great for some animations */
    /*animation-iteration-count: 1;*/
    /*animation-duration: 1s;*/
}

.loader-wrapper.loader-active span:nth-child(1) {}

.loader-wrapper.loader-active span:nth-child(2) {
  animation-delay: 300ms;
}

.loader-wrapper.loader-active span:nth-child(3) {
  animation-delay: 600ms;
}

.loader-wrapper.loader-active span:nth-child(4) {
  animation-delay: 900ms;
}

.loader-wrapper span {
  margin-right: 5px;
  display: inline-block;
  vertical-align: middle;
  background: black;
  width: 10px;
  height: 10px;
}
<div class="loader-wrapper"><span></span><span></span><span></span><span></span></div>

<button id="start_loader">Start</button>
<button id="stop_loader">Stop</button>
Harwill answered 16/12, 2021 at 9:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.