Staggering Animations in AngularJS

ngAnimate now supports staggering animations natively

Published on Dec 18, 2013

1.0 Introduction

Most animation frameworks aim to do the same thing. Whether that be to offer JavaScript or CSS3 animations, a nice and easy to use API, or a quick and dirty integration procedure. Despite all that nice stuff, there is, however, one specific feature that is difficult to properly use and rare to come by ... Staggering Animations.

The idea is simple, but the implementation is difficult. Greensock is one, if not the only, animation framework that does this perfectly, but how do we get this to work with an AngularJS application? With the spectacular new features available in ngAnimate let's have a look to see how we can perform this crazy animation effect within our AngularJS app.

to top

2.0 Staggering Animations?

With the release of AngularJS 1.2, a crazy, but hidden, feature was introduced which provides native staggering support to CSS3 transitions and keyframe animations with just a little extra CSS code**. The $animate service, found within ngAnimate, will do the grunt work to render animations in a staggering fashion.

Still curious as to what a stagger is? Does Greensock ring a bell? Well if not then a stagger is an animation that occurs that contains a delay between each successive animation which causes a "curtain-like" effect to occur. Click here to see how GreenSock performs a stagger animation.

to top

3.0 How do they work?

When a bunch of animations are run at the same time then $animate will automatically queue up everything to make things run faster. So if you have a ngRepeat animation for 20+ elements then all 20 elements will be bundled together and one reflow will happen which will trigger the animation effect for each one all at the same time. Since this reflow queuing operation is so central to $animate's functionality, adding a slight delay for successive elements was relatively painless.

After things are queued up, $animate waits for 10ms and once that timeout has passed then it will trigger the animation for each element. If $animate comes across a timeout delay (via a CSS class) then it generates the transition/keyframe animation delay values for each animation so that they're spaced-out.

All of the stagger operations are handled internally. All that is required to trigger a stagger is just a simple CSS selector with a delay value. Let's have a look at how to do that.

to top

4.0 Staggering CSS3 Transitions

CSS Transitions are supported nicely with ngAnimate and so are staggering CSS transitions. All that is required is to include an additional CSS class the specifies the stagger delay. Let's imagine that we already have a CSS transition animation and we would like to make that animation stagger.

.repeat-animation.ng-enter { /* ... */ }
.repeat-animation.ng-enter.ng-enter-active { /* ... */ }

Let's add in an extra CSS class, .ng-enter-stagger, and specify a delay using transition-delay.

.repeat-animation.ng-enter-stagger {
  /* 200ms will be applied between each sucessive enter operation */
  -webkit-transition-delay:0.2s;
  transition-delay:0.2s;
  
  /* this is here to avoid accidental CSS inheritance */
  -webkit-transition-duration:0;
  transition-duration:0;
}
.repeat-animation.ng-enter {
  -webkit-transition:0.5s linear all;
  transition:0.5s linear all;
  opacity:0;
  line-height:0;
}
.repeat-animation.ng-enter.ng-enter-active {
  opacity:1;
  line-height:20px;
}

Now let's add in the leave and move stagger transition delays as well.

.repeat-animation.ng-enter-stagger,
.repeat-animation.ng-leave-stagger,
.repeat-animation.ng-move-stagger {
  /* 200ms will be applied between each sucessive enter operation */
  -webkit-transition-delay:0.2s;
  transition-delay:0.2s;
}
.repeat-animation.ng-enter,
.repeat-animation.ng-leave,
.repeat-animation.ng-move {
  -webkit-transition:0.5s linear all;
  transition:0.5s linear all;
}
.repeat-animation.ng-leave.ng-leave-active,
.repeat-animation.ng-enter,
.repeat-animation.ng-move {
  -webkit-transition:0.5s linear all;
  transition:0.5s linear all;
  opacity:0;
  line-height:0;
}
.repeat-animation.ng-leave,
.repeat-animation.ng-move.ng-move-active,
.repeat-animation.ng-enter.ng-enter-active {
  opacity:1;
  line-height:20px;
}

Click here to view this animation in action

Notice how transition-duration was also used inside of the stagger CSS class? Why? Well this is here to prevent any accidental CSS inheritance from occurring from the base CSS class (in this case .repeat-animation). So if a stagger animation isn't working properly then chances are that the transition-duration is being accidentally inherited and therefore it must be set to zero for the stagger to actually work.

to top

5.0 Staggering CSS3 Keyframe Animations

CSS keyframe animations are no different from CSS transitions in terms of CSS naming for staggering animations. The only difference is that animation-delay is used instead of transition-delay (for obvious reasons). This separation of styles between transitions and animations allows for both staggering CSS transitions and keyframe animations to be used in parallel with ngAnimate.

.repeat-animation.ng-enter-stagger,
.repeat-animation.ng-leave-stagger,
.repeat-animation.ng-move-stagger {
  /* notice how we're using animation instead of transition here */
  -webkit-animation-delay:0.2s;
  animation-delay:0.2s;
  
  /* yes we still need to do this too */
  -webkit-animation-duration:0;
  animation-duration:0;
}

Since the keyframe animation kicks in only when a reflow has occurred (when the browser repaints the screen) then there may be a slight delay. This is because the keyframe animation doesn't actually start until after the 10ms timeout has run and, depending on if you want to hide the animation before the keyframes start to animate, then some extra CSS code may need to be placed inside of the setup CSS class.

.repeat-animation { /* ... */ }
.repeat-animation.ng-enter-stagger { /* ... */ }
.repeat-animation.ng-enter {
  /* pre-reflow animation styling */
  opacity:0;
}

Click here to view this animation in action

This is pretty damn cool now isn't it!!?

to top

6.0 Staggering JS Animations

With the release of AngularJS 1.2, ngAnimate does not contain native support for staggering animations using JavaScript animations. Much of this has to do with the fact that the stagger feature is a brand new feature in ngAnimate and it would be best to see how well that works and plays out before stretching the feature out to JS animations.

However, we're programmers, and we like to prove ourselves wrong by breaking and rebuilding things. Therefore, let's see if we can do this anyway using JavaScript animations. To get this to work, each successive animation needs to be grouped together into a queue and issued with a timeout. Keep in mind that the animations themselves may get cancelled early and therefore it's important to keep tabs on which animations should run after the stagger is triggered.

var myModule = angular.module('MyApp', ['ngAnimate'])
myModule.animation('.repeat-animation',
  ['$timeout', function($timeout) {
  var queue = {
    enter : [], leave : []
  };
  function queueAnimation(event, delay, fn) {
    var timeouts = [];
    var index = queue[event].length;
    queue[event].push(fn);
    queue[event].timer && $timeout.cancel(queue[event].timer);
    queue[event].timer = $timeout(function() {
      angular.forEach(queue[event], function(fn, index) {
        timeouts[index] = $timeout(fn, index * delay * 1000, false);
      });
      queue[event] = [];
    }, 10, false);
    return function() {
      if(timeouts[index]) {
        $timeout.cancel(timeouts[index]);
      } else {
        queue[index] = angular.noop;
      }
    }
  };
  return {
    enter : function(element, done) {
      element = $(element[0]);
      var cancel = queueAnimation('enter', 0.2, function() {
        element.css({ top : -20 });
        element.animate({ top : 0 }, done);
        var cancelFn = cancel;
        cancel = function() {
          cancelFn();
          element.stop();
          element.css({ top : 0 });
        };
      }); 
      return function onClose(cancelled) {
        cancelled && cancel();
      };
    },
    leave : function(element, done) {
      element = $(element[0]);
      var cancel = queueAnimation('leave', 0.2, function() {
        element.css({ top : 0 });
        element.animate({ top : -20 }, done);
        var cancelFn = cancel;
        cancel = function() {
          cancelFn();
          element.stop();
          //no point in animating a removed element
        };
      }); 
      return function onClose(cancelled) {
        cancelled && cancel();
      };
    }
  }
}]);

Click here to view this animation in action

Our make-shift stagger operation using JavaScript animations is working fairly well. How would this look like with GreenSock?

var myModule = angular.module('MyApp', ['ngAnimate']);
myModule.animation('.repeat-animation',
  ['$timeout', function($timeout) {
 
  var queue = { enter : [], leave : [] };
  function queueAllAnimations(event, element, done, onComplete) {
    var index = queue[event].length;
    queue[event].push({
      element : element,
      done : done
    });
    queue[event].timer && $timeout.cancel(queue[event].timer);
    queue[event].timer = $timeout(function() {
      var elms = [], doneFns = [];
      angular.forEach(queue[event], function(item) {
        item && elms.push(item.element);
        doneFns.push(item.done);
      });
      var onDone = function() {
        angular.forEach(doneFns, function(fn) {
          fn();
        });
      };
      onComplete(elms, onDone);
      queue[event] = [];
    }, 10, false);
    return function() {
      queue[event] = [];
    }
  };
  return {
    enter : function(element, done) {
      element.css('opacity', 0)
      var cancel = queueAllAnimations('enter',
        element, done, function(elements, done) {
      
        TweenMax.allTo(elements, 1, { opacity : 1 }, 0.2, done);
      });
      return function onClose(cancelled) {
        cancelled && cancel();
      };
    },
    leave : function(element, done) {
      var cancel = queueAllAnimations('leave',
        element, done, function(elements, done) {
        TweenMax.allTo(elements, 1, { opacity : 0 }, 0.2, done);
      });
      return function onClose(cancelled) {
        cancelled && cancel();
      };
    }
  }
}]);

Click here to view this animation in action

It can get a bit heavy if we try to this on our own, but com'on we should give ourselves a pat on the back for the crazy JS code we put together. I can't wait to use this once native staggering JavaScript animations are fully baked into the ngAnimate core.

to top

7.0 How to use it with ngRepeat

As stated above with our code examples above, ngRepeat is the prime target for staggering animations. What does the HTML code look like for ngRepeat again?

<div ng-repeat="item in items" class="repeat-animation"></div>

And yes we have already seen how a stagger can be placed on this. But what else can we do? Well, we can also tweak our CSS classes to jump around before the stagger kicks off. The example below shows how to stagger'ize an animation with even and odd classes resulting in an interlace-like animation.

<div>
  <input placeholder="filter" ng-model="f">
</div>
<div ng-repeat="item in items | filter:f"
     class="repeat-animation"
     ng-class-even="'left'"
     ng-class-odd="'right'"></div>
.repeat-animation {
  -webkit-transition:0.2s linear all;
  transition:0.2s linear all;
  position:relative;
  left:50px;
}
.repeat-animation.ng-enter-stagger {
  -webkit-transition-delay:0.05s;
  transition-delay:0.05s;
  -webkit-transition-duration:0;
  transition-duration:0;
}
.repeat-animation.ng-enter {
  opacity:0;
}
.repeat-animation.left.ng-enter {
  left:0px;
}
.repeat-animation.right.ng-enter {
  left:100px;
}
.repeat-animation.ng-enter-active {
  opacity:1; left:50px;
}

Click here to view this animation in action

We're kinda going nuts with all the funky CSS code. But then again we're programmers! It sure is funny how little CSS code overall is required to make such an amazing effect.

to top

8.0 How to use it with ngClass

The ngClass directive also works with staggering animations. But wait a sec? How can we trigger a series of ngClass animations and then apply a stagger to them? Well we may need to use ngRepeat again in this case, but, instead of the stagger effect being triggered on the structural animations (like enter, leave and move) it will be triggered on the class change animation.

<div ng-controller="UsersCtrl">
  <div><input type="search" ng-model="q" placeholder="search items"></div>
  <div ng-repeat="user in users" class="cell focus-animation" ng-class="{on:isMatchAgainstSearch(user)}">
    <h5>{{ user.first_name }} {{ user.last_name }}</h5>
  </div>
</div>
var myModule = angular.module('MyApp', ['ngAnimate']);
myModule.controller('UsersCtrl', ['$scope', function($scope) {
    $scope.users = [
      { "first_name": "April", "last_name": "Cardenas" },
      { "first_name": "Lara", "last_name": "Frank" },
      { "first_name": "Amelia", "last_name": "Vang" },
      { "first_name": "Amos", "last_name": "Mosley" },
      { "first_name": "Brianna", "last_name": "Chan" },
      { "first_name": "Emma", "last_name": "Scott" },
      { "first_name": "Heather", "last_name": "Lawson" },
      { "first_name": "Raja", "last_name": "Valentine" },
      { "first_name": "Mara", "last_name": "Mckee" },
      { "first_name": "Moana", "last_name": "Sloan" },
      { "first_name": "Felix", "last_name": "Livingston" },
      { "first_name": "Odysseus", "last_name": "Ryan" },
      { "first_name": "Denton", "last_name": "Lamb" },
      { "first_name": "Damon", "last_name": "Cotton" },
      { "first_name": "Sasha", "last_name": "Mosley" },
      { "first_name": "Jordan", "last_name": "Franklin"
    }];
    $scope.isMatchAgainstSearch = function(user) {
        return $scope.q.length == 0 || user.first_name.indexOf($scope.q) >= 0 || user.last_name.indexOf($scope.q) >= 0;
    };
}]);
.focus-animation {
  background:#eee;
}
.focus-animation.on-add, .focus-animation.on-remove {
  -webkit-transition:0.1s linear all;
  transition:0.1s linear all;
}
.focus-animation.on {
  background:red;
}
.focus-animation.on-add-stagger, .focus-animation.on-remove-stagger {
  -webkit-transition-delay:0.03s;
  transition-delay:0.03s;
  -webkit-transition-duration:0;
  transition-duration:0;
}
.cell {
  float:left;
  border:1px solid #ddd;
  width:100px;
  height:100px;
  text-align:center;
  line-height:20px;
  font-size:20px;
}

Click here to view this animation in action

Think about using this for a gallery or even a search? The possibilities are endless.

to top

9.0 How to use it with your own directives

As mentioned earlier in the article and as seen with using ngClass, it all really falls down to the inner workings of the $animate service to trigger staggering animations. Keeping this thought fresh in our minds, how can we trigger a stagger in our own directives without using ngRepeat or ngClass? Well remember how the staggering effect works right? As long as enough calls to $animate are made using the same animation event and the same parent element then a stagger will kick off automatically behind the scenes.

myModule.directive('myDirective', ['$animate', function($animate) {
    return function($scope, element) { /* the code below can be used to trigger the stagger animation in a custom directive */
        for (var i = 0; i < 10; i++) {
            var newElement = angular.element('');
            newElement.addClass('stagger-class');
            $animate.enter(newElement, element);
        }
    };
}]);

Now all that needs to be done is to hook up some animation and animation stagger code. But I'm sure we're comfortable with doing that already now aren't we? :)

Click here to learn more about triggering stagger animations

It's great how the stagger is only performed when the CSS class is applied, but our directive code itself is just as normal.

to top

10.0 More to Come

Think of the 80 / 20 rule. If 80% of work to make a website appealing is overall design, sleekness and animations then staggering animations definitely fall into the remaining 20%. Except that in this case it doesn't take four times longer to implement since ngAnimate makes this feature so amazing and, well, perfect!

Have a go with the staggering animations. Let me know how well they work and if you run into any bugs. Please do post these findings in the Github issue tracker instead of here and the Angular team will be happy to know.

to top