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.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Vlad Cazacu
  • 1,520
  • 12
  • 12
  • You can't do that with CSS. You'd need what I suspect would be quick complex Javascript. – Paulie_D Feb 19 '16 at 14:20
  • 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. – Vlad Cazacu Feb 19 '16 at 14:26
  • 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. – Paulie_D Feb 19 '16 at 14:28
  • 1
    It might not be impossible but it would not be straight-forward. You can refer to [this thread](http://stackoverflow.com/questions/34387686/animation-paused-with-transition/34389429#34389429). 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.) – Harry Feb 19 '16 at 14:55
  • 1
    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. – Vlad Cazacu Feb 19 '16 at 15:04

2 Answers2

9

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);
      }
    }
  }
}
Vlad Cazacu
  • 1,520
  • 12
  • 12
  • 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. – Harry Feb 19 '16 at 15:14
  • 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`. – yckart Apr 29 '16 at 11:01
  • 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 great – milpool Mar 19 '19 at 22:49
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>
Luhn
  • 706
  • 6
  • 15