Animations: the Angular Way

Share this:

AngularJS is a major player in the JavaScript MVW framework world. 'Thinking in Angular' is something that can elude developers who come from jQuery or other DOM manipulation heavy libraries. There is an 'Angular way' to do things that is data-driven rather than using DOM traversal to drive view changes, and that can be hard to visualize when it comes to something like animations. Together, we will go through exactly how to animate with the tools provided by the Angular team.

ngAnimate and the $animate Service

The Angular core team gave us the ngAnimate module so that we could give our apps a way to animate from a data driven 'Angular way', and so that we could hook into the events that Angular emits through some of its built-in directives.

Angular, unlike jQuery, focuses on binding our view to a JavaScript object through the use of controllers. This approach allows us to bind view values like input fields directly to a corresponding value in a JavaScript object and trigger view changes through data changes, or vice versa.

So then, just how do we hook animations into these events if they could happen from either the view or the corresponding object being changed?

First, we need to add ngAnimate to our project.

Including Angular Animation in our Project

As of 1.2.0, animations are no longer part of the Angular core, but are instead in their own separate module: ngAnimate. In order to use the $animate service, we need to include the animation library after Angular in our HTML file, like this: js/lib/angular-animate.js

As an alternative, you can also use the CDN or bower to install angular-animate:

$ bower install --save angular-animate

However you choose to install it, make sure to include it in your source file, like so:

Next, we will have to include the ngAnimate module as a dependency to our app. This can be done when we instantiate our Angular app, like so:

angular.module('myApp', ['ngAnimate']);

Now that ngAnimate is included in our project (and as long as we do not have an injector error or something like that in our console) we can begin to create animations with Angular!

CSS3 Transitions

The easiest way to include animations in any application is by using CSS3 transitions. This is because they are completely class-based, which means that the animation is defined in a class and, as long as we use that class in our HTML, the animation will work in the browser.

CSS transitions are animations that allow an HTML element to steadily change from one style to another. To define a transition, we need to specify the element we want to add an effect to, and the duration of said effect.

First, let's take a look at a simple example of a CSS3 transition, and then we can see how to make use of this knowledge from a data-driven Angular app.

Let's create a simple div inside a container div and apply two classes to it: one for basic styling and one for our transition.

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

Now we can add transitions for either the hover state or static state of the element:

This applies two states to our div: one normal state and one for when we hover over the div. The transitions defined in the .rotate and .rotate:hover classes tell the browser how to transition between these two states when we trigger the hover and mouseleave events.

We end up with an effect like this:

Basic CSS3 Transition

Angular Data-Driven CSS3 Animation

Now let's see how we could do something like that in an Angular app, and bind this same functionality to some data within our application.

Instead of doing this transition on :hover, we can create a simple animation by binding transitions to one class, .rotate, and create a class for both the "box" and "circle" states of the div. This enables us to switch between classes using the ng-class directive built into Angular.

Here, we bind the boolean value that is attached to $scope.boxClass to whether or not the element should have the .box or .circleclass. If the boolean is true, then the element will have the .boxclass. If it is false, it will have the .circleclass. This allows us to trigger a CSS3 transition by changing the value of our data, with no DOM manipulation whatsoever.

This does not use the $animate service, but I wanted to provide an example of an instance that you could use CSS3 alone and not have to rely on $animate and ngAnimate.

The result of this is an animation that is triggered strictly by a data change when we change the underlying boolean by clicking the checkbox.

Angular Data-Driven CSS3 Transition

Transitions with $animate

If we want to leverage CSS3 transitions and the $animate service, then we need to know a couple things about how $animate works behind the scenes.

The $animate service supports several directives that are built into Angular. This is available without any other configuration, and allows us to create animations for our directives in plain CSS. To use animations in this way, you do not even need to include $animate in your controller; just include ngAnimateas a dependency of your Angular module.

Once you include ngAnimate in your module, there is a change in how Angular handles certain built-in directives. Angular will begin to hook into and monitor these directives, and add special classes to the element on the firing of certain events. For example, when you add, move, or remove an item from an array which is being used by the ngRepeatdirective, Angular will now catch that event, and add a series of classes to that element in the ngRepeat.

Here you can see the classes that ngAnimate adds on the enter event of an ngRepeat:

ngRepeat Event Classes

The attached CSS classes take the form of ng-{EVENT} and ng-{EVENT}-active for structural events like enter, move, or leave. But, for class-based animations, it takes the form of {CLASS}-add, {CLASS}-add-active, {CLASS}-remove, and {CLASS}-remove-active. The exceptions to these rules are ng-hideand ng-show. Both of these directives have add and remove events that are triggered, just like ng-class, but they both share the .ng-hide class, which is either added or removed when appropriate. You will also see ngAnimate add a .ng-animate class to some of these directives on animation.

Below is a table that illustrates some of the built-in directives, the events that fire, and classes that are temporarily added when you add ngAnimate to your project:

Built-In Directives' $animate Events

Directive

Event(s)

Classes

ngRepeat

enter

ng-enter, ng-enter-active

leave

ng-leave, ng-leave-active

move

ng-move, ng-move-active

ngView, ngInclude, ngSwitch, ngIf

enter

ng-enter, ng-enter-active

leave

ng-leave, ng-leave-active

ngClass

add

ng-add, ng-add-active

remove

ng-remove, ng-remove-active

ngShow, ngHide

add, remove

ng-hide

Angular will automatically detect that CSS is attached to an animation when the animation is triggered, and add the .ng-{EVENT}-active class until the animation has run its course. It will then remove that class, and any other added classes, from the DOM.

Below is an example of using CSS3 transitions to animate a ngRepeat directive. In it, we attach a transition to the base class—.fade in this case—and then piggyback off of the classes that ngAnimate will add to the li elements when they are added and removed from the array. Once again, this allows us to have data-driven animations — the Angular way.

ngRepeat$animate Powered CSS3 Transitions

As we can see, Angular's ngAnimate gives us the ability to easily tap into events and leverage the power of CSS3 transitions to do some really cool, natural animations on our directives. This is by far the easiest way we can do animations for our Angular apps, but now we will look at some more complex options.

CSS3 Animations

CSS3 animations are more complicated than transitions, but have much of the same implementation on the ngAnimate side. However, in the CSS we will be using an @keyframes rule to define our animation. This is done in much the same way that we did our basic transition earlier, except we use the animation keyword in our CSS and give the animation a name like this:

The difference this time, as you can see above, is that we no longer have to use .ng-enter-active or .ng-leave-active, but rather we can attach the animation to .ng-leave and .ng-active and the animation will trigger at the appropriate times because of ngAnimate. This is not a particularly better way to do it than our transition method above, but it illustrates how to use CSS3 animations, which can be MUCH more powerful than this simple effect.

The final result of this animation when applied to our previous grocery list ngRepeat example will look something like this:

ngRepeat $animate Powered CSS3 Animations

JavaScript Animations

Now for the elephant in the room: JavaScript animations with AngularJS.

Angular is entirely data-driven with its fancy two-way data binding—that is, until it isn't. This is one of the most confusing things about coming from jQuery to Angular. We are told to re-learn how we think and throw out DOM manipulation in favor of bindings, but then, at some point, they throw it right back at us later. Well, welcome to that full-circle point.

JavaScript animation has one major advantage—JavaScript is everywhere, and it has a wider acceptance than some advanced CSS3 animations. Now, if you are just targeting modern browsers, then this probably won't be an issue for you, but if you need to support browsers that do not support CSS transitions, then you can easily register a JavaScript animation with Angular and use it over and over in your directives. Basically, JavaScript has more support in older browsers, and therefore, so do JavaScript animations.

When you include ngAnimate as a dependency of your Angular module, it adds the animation method to the module API. What this means is that you can now use it to register your JavaScript animations and tap into Angular hooks in built-in directives like ngRepeat. This method takes two arguments: className(string) and animationFunction(function).

The className parameter is simply the class that you are targeting, and the animation function can be an anonymous function that will receive both the element and done parameters when it is called. The element parameter is just that, the element as a jqLite object, and the done parameter is a function that you need to call when your animation is finished running so that angular can continue on it's way and knows to trigger that the event has been completed.

The main thing to grasp here however, is what needs to be returned from the animation function. Angular is going to be looking for an object to be returned with keys that match the names of the events that you want to trigger animations on for that particular directive. If you are unsure what the directive supports, then just refer to my table above.

We can get rid of any CSS we previously had on the .fade class, but we still need some kind of class to register the animation on. So, for continuity's sake, I just used the good old .fade class.

Basically, what happens here is that Angular will register your animation functions and call them on that specific element when that event takes place on that directive. For example, it will call your enter animation function when a new item enters an ngRepeat.

These are all very basic jQuery animations and I will not go into them here, but it is worth noting that ngRepeat will automatically add the new item to the DOM when it is added to the array, and that said item will be immediately visible. So, if you are trying to achieve a fade in effect with JavaScript, then you need to set the display to none immediately before you fade it in. This is something you could avoid with CSS animations and transitions.

Let's tie it all together and see what we get:

ngRepeat $animate Powered JavaScript Animations

Conclusion

The ngAnimate module is a bit of a misleading name.

Granted, I could not come up with a better name if I tried, but it does not actually DO any animations. Rather, it provides you with access into Angular's event loop so that you can do your own DOM manipulation or CSS3 animations at the proper, data-driven point. This is powerful in its own right because we are doing it 'the Angular way' instead of trying to force our own logic and timing onto a very particular framework.

Another benefit of doing your animations with ngAnimate is that once you write your animations for that directive, they can be packed up nicely and moved on to other projects with relative ease. This, in my book, is always a good thing.

I think agree with you, animation shouldn’t be for the sake of animation, and shouldn’t slow down navigation of a UI. That doesn’t mean I feel that animation has no place in the UI.

I think the delete transition in this example is a good example of unnecessary animation. It is slow, and doesn’t really add to the user experience, because the user interacts directly with the element that is being used.

The move bottom item to top example, however, is good example of animation that improves user experience. If you clicked the button and the bottom item suddenly disappeared from the bottom of the list and reappeared at the top, the change wouldn’t be as noticeable as it is with the subtle animation (which could be a little faster, IMO). Whenever I use animation in the interface I default to 0.1s, because it is just enough time to show a transition while still having that “instant” feel. As a convention, I make the max animation time of UI elements 0.25s. Any longer and the user is waiting.

I think neil is right as well, for the most part. If you EVER feel like “ugh, animation”, that animation shouldn’t have been there. If you don’t notice it, or notice it to the point that it’s delightful or helps you understand something that just happened, it was probably worth it.

This is why my animations or transitions never go beyond the 200ms duration (even that is pushing). Subtlety is the key i think. The user won’t feel like they’re waiting on a UI element to interact with it, but they’ll still see something delightful before they interact with it.

While almost everyone will agree that too much animation is no good, meaningful animation is an incredibly useful tool in modern web design.

Zero animation means static cuts between UI states.

“Just as the shape of an object indicates how it might behave, watching an object move demonstrates whether it’s light, heavy, flexible, rigid, small or large. In the world of material design, motion describes spatial relationships, functionality, and intention with beauty and fluidity.”

Still not sure why to use something this complicated – what’s the problem with jQuery (I mean, besides the fact that there’s an overhyped framework called Angular so we have to use Angular these days)?!??

I’m having a strange problem… Every time I try and run this JS on my server, it runs perfectly until I attempt to move the ‘Mustard’ item to the top…

The ‘move item to top’ button works as expected under most scenarios, but when it gets to mustard, it seems to replace every other item with mustard, until it’s all mustard, and the mustard never goes away. I didn’t want this amount of mustard, what did I do wrong?

There are but few components where animations are an actual improvement – and in most of the cases, those are bound to events that trigger pseudo classes like :hover or :focus, so you can do them without angular.

In my experience, the longest feasible duration of an animation is somewhere between 0.1 and 0.3 seconds, depending on the size, otherwise it will feel sluggish. If you don’t want to take your time to fine-tune the animation, better use a shorter duration and come back to it if you got the time for it.

We just started playing around with Google’s Material Design Angular integration for our animations. Seems as though there are quite a few benefits in implementing Anglular/Material, so far, seems to be an easier method to modifying animations.

👋

CSS-Tricks* is created, written by, and maintained by Chris Coyier and a team of swell people. It is built on WordPress and powered up by Jetpack. It is made possible through sponsorships from products and services we like.