{ "community_link": "https://community.indepth.dev/t/in-depth-guide-into-animations-in-angular/707" }

In-Depth guide into animations in Angular

Explore the various animation methods in Angular, their use cases, implementation, and some tips on performance, debugging, and reusability.

In-Depth guide into animations in Angular

In this post, I’m going to cover the various animations use cases and the different implementations in Angular, using both Angular specific animations and a combination of the more traditional methods and how to use those within an Angular application.

This is a guide on the available animation options and which ones to use when. These includes some basics such as animations for state changes and entrances and exits, some more advanced ones such as route transitions, sequences, keyframes, and queries, and alternatives such as class based, inline, and WAAPI animations.

I’ll also provide some tips and tricks on organizing and optimizing your animation code, and how to use different browsers’ devtools for debugging and analyzing your animation’s performance. Some things to keep in mind to help keep your animations DRY, performant, and easier to debug.

Below are links to a live demo and the source code behind everything that is covered in this article:

High-level Overview

Angular animations (@angular/animations) is a powerful module that comes with Angular which provides a DSL (domain specific language) for defining web animation sequences for HTML elements as multiple transformations over time which could occur sequentially or in parallel. Angular animations use the native Web Animations API, and as of Angular 6, falls back to CSS keyframes if the Web Animations API is not supported in the user's browser.

The animations are based on CSS web transition functionality which means that anything that can be styled or transformed through CSS, can be animated the same way using Angular animations with the added advantage of giving the developer more control in orchestrating it. This provides us with animations that have CSS-like performance along with the flexibility of Javascript out of the box without additional dependencies.

Animations using Angular's BrowserAnimationModule goes through 4 steps. I like to think of this as being comprised of a series of questions - why, what, where, and how, the answers of which being what governs the animation’s behavior:

  • Evaluate data binding expression - tells Angular which animation state the host element is assigned to (why)
  • Data binding target tells Angular which animation target defines CSS styles for the elements state (what)
  • State tells Angular which CSS styles should be applied to the element (where)
  • Transition tells Angular how it should apply the specified CSS styles when there is a state change (how)

JS/CSS Convention

The style function is an integral part of Angular animation, a place to specify what styles to apply to the target element at a certain state. An interesting thing about this function is that it accepts 2 types of conventions, which would explain the varying syntax in animation code you would find on the internet - some having camel case and some with dashed case.

Camelcase

The Javascript naming convention is to use camelcase keys. Angular animation accepts this as is, letting you pass in regular key value pairs like this:

style({
  backgroundColor: "green",
})

Dashed Case

The CSS property naming convention (dashed case), however, has to be enclosed in quotes to stop Javascript from trying to interpret the hyphens as arithmetic operators. So the same code above using the dashed case would look something like this:

style({
  "background-color": "green",
})

Order of Execution

Angular animations happen after what triggers them. For instance, the :enter state change will get called after ngOnInit and the first change detection cycle whereas :leave would happen right after the element ngOnDestroy is called.

In addition, each time an animation is triggered, the parent animation will have priority over the children, blocking all child animations from executing unless explicitly stated to execute both. In order to run both animations, the parent animation must query each element containing the child animations and run it using the animateChild method which is covered in more detail here.

Setup

In order to use @angular/animations in your application, you will have to do the following:

  • Verify that @angular/animations package is installed and listed as a dependency in your package.json (it should be included by default)
  • If not, add it by running npm install --save @angular/animations
  • Import BrowserAnimationsModule and add it to the module's imports array (see snippet below)
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
  imports: [
    BrowserAnimationsModule
  ],
})

Note: Angular also comes with NoopAnimationsModule which you can use to disable all animations globally.  It is more commonly used for testing to mock the real animation when the animations are either too slow or doesn't play any role in what is being tested.

Basics

Below are a few basic use cases of the Angular animation module, more advanced use cases will be covered in the subsequent section. Before diving into the use cases, let’s start with getting a high level understanding of animation states which will be used in most sections of the post.

Animation States

Angular lets you define a style and transition to be applied when an element’s state changes. Angular provides 3 different states which we could use in our animation code:

  • Wildcard (*) - this is the default or all states of the element, for example active => * represents a state change from active to anything else.
  • Void (void) - this is the state when the element is created but not yet part of the DOM, or when the element is removed from the DOM.
  • Custom - this can be any custom name to indicate a certain state of the element (example: 'active', 'inactive').

Transitions between State Changes

Before we start, we will need to define the different states for the element to transition between. This will be the first parameter that is passed in to the state function (which in the example below are ‘default’ and ‘disabled’), along with the style that needs to be applied when the element is in that state.

To animate the transition between the different state, we will need to pass in the transition function specifying the 2 states (* => * in the example below means anything to anything, we can use a more specific target too such as default => disabled depending on your requirements) the transition needs to be applied to, and the animation function that needs to be executed during the transition.

import { trigger, state, style, animate, transition } from '@angular/animations';

@Component({
  ...
  animations: [
    trigger('enabledStateChange', [
      state(
        'default',
        style({
			opacity: 1,
		})
	),
	state(
		'disabled',
		style({
			opacity: 0.5,
        })
      ),
      transition('* => *', animate('300ms ease-out')),
    ])
  ]
})

Here is a brief explanation of the methods used in the above snippet:

  • trigger - accepts a name for the animation trigger and an array of state and transition methods to configure the animation
  • state - accepts a name for the state and the styles that should be conditionally applied when in specified state
  • style - CSS styles to be applied
  • transition - specifies the configuration for transitioning between the different states and its direction
  • animate - specifies the duration and any additional CSS animation properties such as easing

style, transition, and animate accept both grouped (as an array) and singular arguments giving us some flexibility in terms of configuring our animations.

In your template, all you need to do is add the animation name defined previously, prefixed with @ and bind it to a variable that will toggle between the states and Angular handles the rest.

<div [@enabledStateChange]="stateOfElement">...</div>
Demo state change animation

Enter And Exit Animations

Angular also provides some useful aliases such as :enter and :leave to animate elements entering and leaving the DOM. These aliases are essentially transitions to and from the void state, i.e. void => * and * => void respectively. This is particularly useful for adding some animation to elements which are shown conditionally using *ngIf or *ngFor. The code below shows how you can create a fade in and fade out animation.

trigger('fadeSlideInOut', [
	transition(':enter', [
		style({ opacity: 0, transform: 'translateY(10px)' }),
		animate('500ms', style({ opacity: 1, transform: 'translateY(0)' })),
	]),
	transition(':leave', [
		animate('500ms', style({ opacity: 0, transform: 'translateY(10px)' })),
	]),
]),

And to use it in your template, all you need to do is add the trigger name prefixed by @. Since it is exclusively using the :enter and :leave aliases, we don't have to bind it to anything.

<div *ngIf="show" @fadeSlideInOut>...</div>
Demo enter exit animation

Advanced

As you can see in the basics section, a lot of the common use cases are fairly straightforward to implement. Here I will go over some of the more advanced and perhaps less common use cases which could be useful for some scenarios.

Target Multiple Elements using Queries

The previous sections have primarily focused on targeting single elements where the animation trigger is applied to. If we have an animation set that we want to apply to a group of elements all at once with the same trigger, we can do so using the query function. An example of this would be a list that applies the same animation to each list item as it is added to the DOM.

A difference of using query compared to targeting a specific element is where the trigger is applied to. When using query, the animation trigger will be applied to the parent, where the query function will look for elements that meet the query parameters within the parent (including nested children) which can then be used to execute some animation. Out of the box, query accepts the following tokens:

  • :enter and  :exit - returns all elements that are inserted or removed
  • :animating - returns all elements that are currently animating
  • :self - returns current element
  • @{animationName} - returns all elements with a matching animation trigger

You can also query multiple of these properties together by passing in a comma separated string of the tokens to the query function. As I mentioned earlier, Angular lets you do with the queried elements essentially the same as when you target specific elements directly. The second parameter for the query function accepts either a single AnimationMetadata or an array of AnimationMetadata, which means that it's possible to orchestrate complex animation sequences or logic within a query function to target multiple elements.

Below is an example of how you would apply a ShakeAnimation to all the children elements using the query function.

const ShakeAnimation = [
	style({ transform: 'rotate(0)' }),
	animate('0.1s', style({ transform: 'rotate(2deg)' })),
	animate('0.1s', style({ transform: 'rotate(-2deg)' })),
	animate('0.1s', style({ transform: 'rotate(2deg)' })),
	animate('0.1s', style({ transform: 'rotate(0)' })),
];
export const QueryShake = [
	trigger('queryShake', [
		transition('* => default', [query('.card', ShakeAnimation)]),
	]),
];
Demo query multiple elements animation

Limiting the Number of Elements Queried

Piggybacking off the previous section, Angular's animation module also gives you the option to limit the number of elements you want to animate from your query, with the ability to do a negative query: querying n number of items starting with the last element.

This is particularly useful if you want to add some animations only to the first few or the last few of a set of dynamic elements (such as elements that are created with *ngFor). Using the previous example's ShakeAnimation, we can add an additional limit property to the query function passing in the number of elements to be returned.

export const QueryShake = [
	trigger('queryShake', [
		transition('* => withLimit', [
			query('.card', ShakeAnimation, {
				limit: 2,
			}),
		]),
	]),
];
Demo query multiple elements with limit animation

Animating Children Elements

Angular animation comes with a handy function called animateChild() which as the name suggests, executes the child’s animation. You might be asking why would we need this if we can execute the child’s animation independent of the parent?

One of the common use case for this is when you have an *ngIf attached to the parent and each of the children has its own animation triggers attached to it with their own enter and leave animations. This is not a problem when the parent enters the DOM, all the children elements’ animation will be executed normally as they are added to the DOM. However, the leave animation on the children elements doesn’t really work the same way. Because the *ngIf is on the parent, once that boolean becomes false, the children will immediately be removed from the DOM along with the parent without executing their animation and waiting for it to be done before removing it. A way to handle that scenario is to attach a trigger to the parent and querying the children as part of the parent’s animation. Below is an example of how we would use the parent’s animation to trigger its children’s animation.

Let’s say we have a simple container with 2 children, each with its own set of animations (different triggers) with the following structure.

<div *ngIf=”isDisplayed” @container>
	<div @enterExitLeft></div>
	<div @enterExitRight></div>
</div>
export const EnterExitLeft = [
    trigger('enterExitLeft', [
        transition(':enter', [
            style({ opacity: 0, transform: 'translateX(-200px)' }),
            animate(
                '300ms ease-in',
                style({ opacity: 1, transform: 'translateX(0)' })
            ),
    	]),
	    transition(':leave', [
            animate(
                '300ms ease-in',
                style({ opacity: 0, transform: 'translateX(-200px)' })
            ),
	    ]),
    ]),
];
export const EnterExitRight = [
    trigger('enterExitRight', [
        transition(':enter', [
            style({ opacity: 0, transform: 'translateX(200px)' }),
            animate(
                '300ms ease-in',
                style({ opacity: 1, transform: 'translateX(0)' })
            ),
        ]),
        transition(':leave', [
            animate(
                '300ms ease-in',
                style({ opacity: 0, transform: 'translateX(200px)' })
	        ),
        ]),
	]),
];

To be able to trigger all the children’s animation using the parent container’s *ngIf, we will need to do a query with a wildcard to get all the children’s triggers, followed by the animateChild() function to tell Angular to execute the animation that it finds on the queried elements.

export const Container = [
	trigger('container', [
		transition(':enter, :leave', [
			query('@*', animateChild()),
		]),
	]),
];

What the code above does is it tells the parent to find all the children of the element with an animation trigger (anything that starts with @) attached to it, and run the animation as part of the parent’s animation sequence. In the code above, I used a wildcard prefixed with @, which returns all the children with an animation trigger. This might not be applicable to all use cases. For cases where you need to target specific children or maybe target different child or child animations depending on a certain condition, we can pass in a different target here instead of @* as the query parameter depending on your needs.

Demo children animation

Route Animations

Route animations refers to the animations that are applied to view transitions during a route change. As per the Angular docs, this is done by defining a nested animation sequence in the top-level component that hosts the view and the components that host the embedded views. This could also be applied to nested router-outlets in your application, the animation trigger just needs to be applied to the div that wraps the router-outlet.

To demonstrate this, we will first need to wrap the router-outlet inside a div which will contain the trigger for the animation. Then add an attribute directive in the router-outlet that contains data about active routes and their states which is then used to assign an animation state value to the animation trigger based on the route configuration.

<div [@routeAnimation]="prepareRoute(outlet)">
	<router-outlet #outlet="outlet"></router-outlet>
</div>

Secondly, we need to pass the outlet’s current state to our routeAnimations using the router-outlet’s activatedRoute property. This property will get updated every time a navigation occurs which in turn will trigger our animation. We will use a helper function prepareRoute to do the necessary checks and return the value required by the routeAnimation trigger.

prepareRoute(outlet: RouterOutlet) {
	return outlet?.isActivated || '';
}

When an animation is triggered, we will have access to the previous page through the :leave selector and the current page through the :enter selector. We can use these the same way as we would when we animate individual elements. With that said, you would also be able to apply the different sequences as described here to your animation. Below is an example of what a fade in and fade out route animation definition would look like.

const resetRoute = [
    style({ position: 'relative' }),
    query(
        ':enter, :leave',
	    [
        	style({
                position: 'fixed', // using absolute makes the scroll get stuck in the previous page's scroll position on the new page
                top: 0, // adjust this if you have a header so it factors in the height and not cause the router outlet to jump as it animates
                left: 0,
                width: '100%',
                opacity: 0,
            }),
    	],
	    { optional: true }
    ),
];

// Fade Animation
trigger('routeFadeAnimation', [
    transition('* => *', [
        ...resetRoute,
        query(':enter', [style({ opacity: 0 })], {
        	optional: true,
        }),
        group([
            query(
                ':leave',
                [style({ opacity: 1 }), animate('0.2s', style({ opacity: 0 }))],
                { optional: true }
            ),
            query(
                ':enter',
                [style({ opacity: 0 }), animate('0.5s', style({ opacity: 1 }))],
                { optional: true }
            ),
        ]),
    ]),
]);

The elements in the query array are executed in the order they are in (looking at the code above, it will be executed from top down). The first resetRoute gets executed first which will hide and set some properties to both the previous and current view to allow them to overlap. Both the views will be present in the DOM at the same time (the view that is being navigated to appears immediately instead of appearing after the view being navigated from has disappeared) preventing them from stacking up and breaking the layout. This is followed by the actual animations for the entering view and the leaving view.

There is no difference in writing animation code for route animation and animation that targets regular html elements or Angular components. Therefore, we could use all of the animation properties that we would normally use on an element and apply to our route animation as we see fit.

Variable Route Animations

We can also pass in additional parameters through the router’s data property if we need variable animations. A common use case for this is if we want to trigger different enter and exit animations for different routes.

{
	path: 'home',
	component: HomeComponent,
	data: { animation: 'home' },
},
{
	path: 'post',
	component: PostComponent,
	data: { animation: 'post' },
}

In order to get the additional parameter and use it in our animation, we will have to modify the prepareRoute function to return the additional parameter. Instead of returning the router’s state, we will use the activatedRouteData property to access the data object and select the animation property.

prepareRoute(outlet: RouterOutlet) {
    return (
        outlet?.activatedRouteData &&
        outlet.activatedRouteData['animation']
	);
}

We can then use the additional parameter in our animations array, treating them as different states that we can transition to and from like so:

trigger('routeAnimation', [
    transition('home => post', []),
	transition('post => home', []),
]);

Demo route animation

Disable Animations

Sometimes we want to disable an animation when a certain condition is met, for example, on low performing devices, certain browsers, when the user has the system setting set to minimize the amount of non-essential motion (prefers-reduced-motion media query), or maybe an internal setting within the application. Angular provides you with a @.disabled property that lets you do exactly this. This property lets you pass in an expression to conditionally disable and enable children animations, defaulting to true if no expression is passed in.

<div [@.disabled]="disableAnimationCondition">
	<div [@animate]="expression">Animate</div>
</div>

This property disables all the animation on the element itself and all the children of the element, including those that are rendered from within a router outlet. Under the hood @.disabled adds/removes .ng-animate-disabled class on the element where it's applied. This allows us to either disable animations on a specific component, certain sections of the application or even application wide.

Toggling the disabled state for the entire application can be done by adding the disabled property through a HostBinding on the top level AppComponent like the snippet below. This will disable all the animations throughout the application, with a few exceptions that will be covered in the following section.

export class AppComponent {
	@HostBinding('@.disabled') private disabled = true;
}
Demo disable animation

Some gotchas of this property

The disabled property only targets Angular animations, hence animations that are implemented using CSS transitions or keyframe animations won’t be disabled.

Another caveat: it won’t work for elements that are appended to the DOM directly. Some examples of these types of elements are overlays such as bottom sheets and modals. Instead of using the previous methods of adding the disabled property, we can use Angular’s Renderer2 to set the attribute directly to the overlay containers to disable both the element and its children’s animations.

constructor( private overlayContainer: OverlayContainer, private renderer:Renderer2 ) {
	const disableAnimations:boolean = true;
    // get overlay container to set property that disables animations
    // Note: how to get the container element might vary depending on what the element is
    const overlayContainerElement:HTMLElement = this.overlayContainer;
    // angular animations renderer hooks up the logic to disable animations into setProperty
    this.renderer.setProperty( overlayContainerElement, "@.disabled", disableAnimations );
}

Elements that are added to the DOM directly could alternatively be disabled by importing the NoopAnimationsModule instead of the regular BrowserAnimationModule in the module that contains these components, which mocks the animations. However, this disables all the animation within that module.

Think of it like turning off your TV by flipping the lever on your circuit breaker. This might work for certain use cases such as when you want to disable all 3rd party animations within a module, but this would probably not work well for something that is more dynamic.

Animation Sequences

Animations can run both in sequence and in parallel using the functions sequence() and group() respectively. A combination of parallel and sequence can also be used where the animation runs with a cascading delay between the elements. This effect is achieved using stagger().

group and sequence are a little different compared to stagger. The former are applied to animation steps (values inside the animation array), whereas the latter are applied to the animated elements.

To demonstrate the various animation sequences, let’s start with defining the template which contains the parent element that we will target in our animations along with a few children elements. This is commonly used in lists or grid-like components containing multiple same or similar children elements. For simplicity, we will animate the children elements entering the view, adding a fade in and grow effect using the 3 sequences.

<div @fadeInGrow>
    <div>First Element</div>
    <div>Second Element</div>
    <div>Third Element</div>
</div>

Run Animations in Parallel

group lets you run multiple animation steps in parallel. An example of a use case for this is if you want to animate multiple properties with varying animation properties such as different duration, delay or eases.

animations: [
    trigger('fadeInGrow', [
        transition(':enter', [
            query(':enter', [
                style({ opacity: 0, transform: 'scale(0.8)'  }),
                group([
                    animate('500ms', style({ opacity: 1 }),
                    animate('200ms ease-in', style({ transform: ‘scale(1)’ })
                ])
            ])
        ])
    ])
]
Demo group animation

Run Animations in Sequence

sequence works similar to group where it alters the animation steps execution. sequence runs the animation sequentially, executing animations in the animation array one after the other. This function simplifies the process of chaining animations for a single target element.

Comparing the code below and the code in the previous section, everything looks identical, except the group function, which is replaced with the sequence function. It works the exact same way as running in parallel, the difference being sequence will tell Angular to execute the animation one after the other. Instead of fading in and growing the element at the same time, the transform animation will be executed after the opacity animation is done.

animations: [
    trigger(‘fadeInGrow’, [
        transition(‘:enter’, [
            query(‘:enter’, [
                style({ opacity: 0, transform: ‘scale(0.8)’  }),
                sequence([
                    animate(‘500ms’, style({ opacity: 1 }),
                    animate(‘200ms ease-in’, style({ transform: ‘scale(1)’ })
                ])
            ])
        ])
    ])
]
Demo sequence animation

Stagger Animations

Unlike the previous 2 functions, stagger is applied to the animated elements. This is usually used in conjunction with the query function to find inner elements within a parent/container element and applying animation to each of the child individually. What makes stagger unique is that it takes in an additional parameter timing to specify the delay for the animation’s execution for each element creating a cascading effect.

With stagger, the animation will be applied in the order of the element queried. This usually results in a staggering effect from the top down. We can easily reverse this order by passing in a negative value to the timing parameter resulting in the animation staggered starting from the last element and making its way up.

The second parameter in the stagger function accepts an array of style and animate functions, which means that we could also use the previous sequences - sequence and group to time the individual step of the animation together with stagger controlling the timing of the individual elements.

animations: [
    trigger(fadeInGrow, [
        transition(‘:enter’, [
            query(‘:enter’, [
                style({ opacity: 0 }),
                stagger(‘50ms’, [
	                animate(‘500ms’, style({ opacity: 1 })
                ])
            ])
        ])
    ])
]

Let’s break down the new functions in the code above:

  • The trigger fadeInGrow targets the parent adding an :enter transition which will execute the animation in the transition array when the parent element enters the DOM.
  • query(':enter') inside the transition array targets all the children elements which will enter the DOM and applies the properties in the array that gets passed in which defines the elements’ styles and animations.
  • stagger('50ms') in the array being passed in to the query function tells Angular to execute all the animations applied to the children elements with a 50 ms delay between each element.
Demo stagger animation

Multi-step Animation using Angular Keyframes

Similar to how CSS keyframes animations work, keyframes allow us to build an animation in multiple steps. In other words, it lets us sequence our style changes for each element. Since this method can be passed in to the animate function, it can be combined with the previous section’s animation sequences - group, sequence, and stagger, giving us even more control over the sequencing of our animations.

Angular’s keyframe function comes with an offset property which accepts decimals ranging from 0 to 1 to specify the steps of our animation. These are identical to the CSS keyframe counterparts of using percentages or to and from properties that we normally use to specify our animation steps. Below is an example of a simple CSS keyframe animation, and what it looks like when using Angular’s keyframe function.

/* css */
@keyframes 'fadeSlideGrowKeyframe' {
    30% { transform: opacity(1)’ }
    60% { transform: ‘translateY(0)’ }
    100% { transform: ‘scale(1)’ }
}
/* angular animations */
trigger('fadeSlideGrowKeyframe', [
    transition(':enter', [
        style({ opacity: 0, transform: 'scale(0.5) translateY(50px)' }),
        animate(
            '500ms',
            keyframes([
                style({ opacity: 1, offset: 0.3 }),
                style({ transform: 'translateY(0)', offset: 0.6 }),
                style({ transform: 'scale(1)', offset: 1 }),
            ])
        ),
    ])
])
Demo multi-step keyframe animation

Implementation Tips

Reusing your animation

A lot of times some animations get reused in several places in the application which tend to lead to duplicated animation code in several components. We could abstract our animation code in a few different ways depending on the use case which I will show below to keep our animation code as DRY as possible.

Abstracting the entire animation trigger

This is probably the most straightforward way if there are no configurable pieces in your animation and you want to keep the naming and behavior of the animation consistent across all your components. You can abstract out your entire trigger into a separate file and use a combination of different triggers in the animations array in the component's decorator by passing in the imported animations.

// fade.animation.ts
export const Fade = trigger('fade', [
    transition(':enter', [
        style({ opacity: 0 }),
        animate('500ms', style({ opacity: 1 })),
    ]),
    transition(':leave', [animate('500ms', style({ opacity: 0 }))]),
]);
import { Fade } from './fade.animation';

@Component({
	animations: [Fade],
})

Using the AnimationReferenceMetadata

This approach lets you pass in additional parameters to your animation making it configurable depending on the caller. A limitation to this is that it only works with pre compiled values. In other words, you won't be able to modify the parameters at run time, for instance with the element's current position. If you need to be able to pass in run time information, this is where I would recommend using AnimationBuilder and AnimationPlayer instead. There is a great article by GrandSchtroumpf which covers a workaround that lets you use AnimationBuilder combined with AnimationReferenceMetadata to be able to use dynamic values (with some known limitations).

export const Slide = animation([
    style({ transform: 'translate({{x}}px, {{y}}px)' }),
    animate('{{duration}}s', style({ transform: 'translate(0,0)' })),
]);

// use the animation from within the trigger
trigger('slide', [
    transition(
        ':enter',
        useAnimation(Slide, {
            params: {
                x: 0,
                y: 50,
                duration: 0.3,
            },
        })
    ),
]),

The major difference here compared to a regular animation is the use of the useAnimation method in place of the array of animations, which accepts the animation we created and a params object with any additional parameters that the animation might expect.

Disable Animations when Testing

If you aren’t testing the animations itself, instead of using the BrowserAnimationModule which will run your animations like the real application (which might not be useful for the unit tests and might even slow down the execution of your test cases), you could import and use Angular’s NoopAnimationsModule instead. As the name suggests, noop (no-operation) is a utility module which mocks the real animation but doesn’t actually animate it.

@NgModule({
    imports: [
        // BrowserAnimationsModule // when running the main application
        NoopAnimationsModule // when running tests
    ]
})

Animation Performance

Maintaining a 60 fps frame rate when you are animating is very important as anything less will result in a noticeable stutter or what is commonly referred to as jank. The key here is to be able to identify which properties are expensive to animate and which aren't and utilizing the compositor thread wherever possible. We will go over some additional metrics that we should be aware of when writing our animation code in the next section.

UI have a specific drawing sequence which are as follows (top being the first and bottom being the last in the sequence):

  • Styles (margin, padding, etc.)
  • Layout (height, width, etc.)
  • Paint (background, color, visibility, etc.)
  • Composite (opacity, transforms - rotate, scale, translate, etc.)

The earlier you are in the sequence, the more expensive it is to animate since everything following it will have to be executed. Layout changes are particularly expensive if you have a lot of elements on the page as it could potentially trigger a lot of recalculation to happen. For example, if you have 10 elements on your page, animating the width/height of the first element will cause the 9 other elements to move or change size to accommodate the new width of the first element. transforms and `opacity on the other hand are relatively cheap as those are on the composite step of the drawing sequence.

Here is a comprehensive list of what each CSS property will trigger that you can refer to as you are writing your animation.

CSS Animations and Web Animations both use the compositor thread which is independent of the main UI thread. That means, even if the main thread is doing some heavy task, your animation wouldn't be affected since it's on a different thread. Angular Animations uses Web Animations APIs and CSS Animations, which means this is less of a concern. However, animation that requires paint or layout will still utilize the main thread. This, depending on how much work the main thread is going, could result in some stutter.

With that said, to have a silky smooth animation and avoid skipped/dropped frames, it is important to optimize your animations. Make sure you know the hidden implications and potential performance hit of the properties you are animating. Wherever possible, avoid animating properties that would trigger layout or paint and try to stick to composite properties (opacity, rotate, translate, and scale).

Performance Tooling

I really like how Liam DeBeasi breaks down some of the key performance metrics that we should pay attention to when building our animations and how we can use tools in modern browsers to help us visualize these metrics for our application during his talk at Ioniconf 2020. These metrics includes:

  • Average frames per second (FPS)
  • Main thread processing
  • Average CPU usage and energy impact

We can utilize a couple of different tools to ensure that our animations are optimized and are being run efficiently. Let’s dive in a little deeper on each of the points above, what we should be the goal metric we want to aim for, and how we can utilize the various tools to ensure that we are meeting these goals.

Average frames per Second

We want this to be as close as 60 FPS as possible. Anything below 60 FPS will be noticeable to the users and will result in stutter commonly known as jank.

We can see the actual frames per second during the animation’s execution using Chrome devtools’s performance tab. You will need to hit the record button and run your animation. Try to leave some time before and after your animation when you record to make sure that your timeline doesn’t get cut prematurely before your animation has started/ended. If you click on the Interactions dropdown, you will see an option for Animations. This will show you where in the timeline that animation happens.

You can use this information to identify which section in the timeline the animation is happening and either select that section on the timeline to view the average fps over that duration or hover over the Frames section of that period in the timeline to get the actual frames per second data.

Average FPS over the duration of the animation (Chrome devtools)
FPS at a point in time during the animation (Chrome devtools)

One way to make sure our animation code is hitting the 60 FPS target is to make sure that all the animation is being optimized by the browser and a good tool to use here is Firefox’s animation inspector. It shows a synchronized timeline giving us a top down view of the animations. Below is what you would see on the animations tab of the inspector when a page has a running animation.

Animations tab showing a timeline view (Firefox devtools)

Some things to note here are the colors of the charts on the main timeline view. The colors indicate different type of animations/transitions:

  • Green - web animations
  • Orange - CSS animations
  • Blue - CSS transitions

Each chart can also be clicked on to get a more detailed view on what individual properties are being animated in that animation.

You will also notice a gray thunderbolt icon on the right of the chart on the main timeline view and a green thunderbolt on the script animation view below the main timeline view. This is to indicate if the animation being run is optimized by the browser or not. The animations or the individual properties that have a thunderbolt icon as shown in the image above means that the animation or property is optimized by the browser.

What we want to do based on this data is to make sure that all (if not, most) of our animations/properties are optimized and should show up with the thunderbolt icon.

Main thread processing

The main thread is responsible for a lot of things such as layout and paint (in terms of UI) and also evaluating javascript. We want to keep this to a minimum to free up the main thread for our application to perform other tasks that do require the main thread.

We can use Chrome and Safari’s Dev tools to test this metric, each tool giving us a different but equally useful insight to this metric.

Let’s start with Chrome Devtools. We can use the performance tab similar to the previous section, but instead of focusing on the frames, we will highlight the section of the timeline that has the animation and select the main option from the sidebar. This will show the main thread usage over this period. As you can see in the image below, Chrome devtools gives us the percentages and milliseconds of each of the processes that the main thread is running. The goal here is to keep painting and rendering to be at a minimum and the main thread to be mostly idle during the animation duration. This will make sure that our animation code doesn’t interfere with other processes that are running on the main thread which could cause the animation to drop frames or stutter.

Main thread activity information during the duration of the animation (Chrome devtools)

Safari’s Devtools on the other hand provides a slightly different way of looking at this where it displays the activities on the different threads that are currently running over the duration of the animation. We will need to click on Start Timeline Recording from the Develop menu or the red record button on the top left if you have the developer tools open. We can then start our animation as it is recording and stop the recording after we’re done with the animation to analyze the data. This should display a timeline of what is happening during the recorded period. Click on the CPU section of where the animation is happening in the timeline to focus on the thread activities. This will show both the main thread usage over time and also a chart of the amount of activities each thread is doing over time. Similar to our data analysis from Chrome devtools, we want to keep the amount of activity that the main thread is doing related to our animation to a minimum.

Main thread activity information during the duration of the animation (Safari devtools)

Average CPU usage and energy impact

Like main thread processing, we want to keep average cpu usage at a minimum as CPU usage has a direct impact on energy impact. In other words, the higher the average CPU usage, the higher the energy consumption which will result in faster draining of batteries.

We can use Safari’s timeline feature to view the CPU usage and energy impact over a period of time. We will need to click on the record button on Safari’s devtools and trigger the animation in order to analyze it.

If you look at the image below, you can select individual sections from the timeline to view what is happening within that time period. Clicking on the ‘CPU` option on the left will display some additional details about the main thread usage and its energy impact. These are properties that we want to keep at a minimum. For energy impact, we want the needle in the energy impact dial to fall closer to the left end towards the green (low) and keep the ‘Average CPU’ usage at a low percentage.

Something to note here is that in a real application there could be a lot of other processes running that would affect these metrics which are unrelated to the animation code itself, so it’s important to try to isolate the process which are animation specific or test the animation code independently to get an accurate measurement.

Average CPU usage and energy impact (Safari devtools)

Safari’s develop menu might be hidden by default, so if you don’t see a 'Develop' option on your menu bar, you will have to go to 'Preferences > Advanced' and check the option to 'Show Develop menu in menu bar'.

Debugging

Both Chrome and Firefox’s devtools come with some powerful animation debugging tools which are really handy to use as we build out animations. These tools let you slow down, replay and inspect the source code for your animation. Both browsers’ devtools also lets you modify some of the animation properties on the fly and replay your animation with the modified properties.

Chrome

Chrome has 2 main features that in my opinion are very helpful when it comes to debugging your animation code. I usually always find myself doing some fine tuning of the animations through the devtools and copying that code over to my code editor.

Animation Inspector

Before we dive into how to use the Chrome’s animation inspector, let me show you where you can find this amazing tool. Chrome has the animation inspector option under the more tools submenu option.

Where to find Chrome's Animation Inspector

At the time this post is written, Chrome only supports CSS animations, CSS transitions, and web animations. You wouldn’t be able to use this if you are using `requestAnimationFrame` for your animation.

If you have the animations tab open on your devtools, you should be seeing blocks of animation groups added to the top as your animation gets triggered on your application. Clicking on the block will open up a more detailed view of what the actual animations that are being executed as shown in the image below.

Let’s break down the animations tab view further and discuss some of the key features

  • Controls - lets you play, pause and modify the speed of the animation
  • Animation groups - shows the different group of animations that were executed. The animation inspector groups the animations based on start time (excluding delays) predicting which animations are related to each other. From a code perspective, animations that are triggered in the same script block are grouped together.
  • Scrubber - you can drag the red vertical bar left and right to display the state of the animation at that time in the timeline
  • Timeline - shows a breakdown of the elements in the DOM that are being animated in the animation group and the timeline for each element’s animation
  • 2 solid circles- these 2 circles mark the beginning and end of the animation. It’s possible to see multiple instances of these for cases where the animation runs for multiple iterations, where these solid circles will mark the start and end of each iteration
  • Highlighted section - the animation duration
  • Hollow circle - timing of keyframe rules if the animation defines any (see 2nd to 5th element in the image below)

All the components in the timeline for each element can be modified by dragging them horizontally. We can modify the duration by moving the start and end solid circles, add delays by moving the highlighted section and modify keyframe timings by moving the hollow circle. We can then view the updated animation changes by clicking on the replay button to rerun the animation group.

Animations inspector (Chrome devtools)

Bezier Curve Editor

If you are using CSS keyframes in your animation (this is covered in the later sections of the post), Chrome devtools also has a tool to edit the curves of your animation dynamically using Lea Verou’s cubic bezier visualization.

This is extremely helpful as you no longer have to go back and forth between your editor and your browser to tweak the bezier curves to get the right timing, you can do it all in your browser. Use the replay button on the animations tab to replay the animation with the updated bezier curve. Access this feature by clicking on the squiggly line icon on the animation property of your element. Below is an image of how to access the bezier curve from your animation.

Bezier curve editor for keyframe animations (Chrome devtools)

The purple circles attached to the purple lines on the bezier curve editor are draggable vertically and horizontally to edit the curve of the line which in turn will update the cubic-bezier function. You can see a quick visualization of what the timing function looks like from the purple circle towards the top of the popup, showing how the animation will accelerate/decelerate over time.

Firefox

Firefox’s devtools has almost identical functionality as chrome’s devtools in terms of its animation inspector and bezier curve editor. I won’t go in detail on how each of these works since it is more or less covered in the previous section, however, I will add a couple of screenshots of how these look on Firefox’s devtools so you get an idea of what to expect when using Firefox to debug your animations.

Animation inspector (Firefox devtools)
Bezier curve editor for keyframe animations (Firefox devtools)

Alternative to Angular's Animation Module

Besides Angular's animation module, Angular also gives you the flexibility to use a couple different ways to write your animation. Some of these are slightly modified common patterns you would see in a regular vanilla application whereas some are more Angular specific.

Class based animations

Since angular runs on browsers and utilizes HTML and CSS, we can leverage CSS animations in our Angular application which works the exact same way as how it works in a vanilla HTML CSS application. You would add a class to an element based on a certain condition which will then trigger an animation using CSS either through CSS transitions or keyframes.

The CSS code for both cases will be identical and could be something as simple as the following for CSS transform:

#targetElement {
	transition: all 0.5s;
}
#targetElement.shrink {
	transform: scale(0.8);
}

and the following for CSS keyframes:

#targetElement.shrink {
    animation: shrink 1s;
}

@keyframes shrink {
    0% {
	    transform: scale(1);
    }
    100% {
    	transform: scale(0.8);
    }
}

The main difference here is how you can easily add and remove classes using angular. For example, let's say we want to add a class called 'shrink` when the isSelected boolean is true, in Javascript, it would look something like this:

var element = document.getElementById("targetElement");

if (isSelected) {
	element.classList.add("shrink");  // to add a class
} else {
	element.classList.remove("shrink");  // to remove a class
}

This can be handled directly in the template using Angular by attaching a condition to the class. Below is a sample of how it would look in an Angular template:

<div [class.shrink]="isSelected"></div>
Demo class based animation

Similar to Angular animation’s @animation.done event, class based animation also comes with some events which we can hook into. Depending on whether you are using keyframes or transitions, we can use either animationend or transitionend to listen to the animation end event.

The nice thing about this approach is that you would be able to utilize any CSS animation library that works based on adding and removing classes such as animate.css or magic.css. Chris Coyier has an amazing article that lists some of the popular ones if you are interested.

Inline Animations

Basically the same as the class based animation with the exception that the animation code itself is written in the template instead of as part of a class in the CSS file. This is particularly useful if you have some parts of the animation code that needs to be dynamic in a way where a certain transformation value needs to be calculated based on some predetermined external factor. An example of this would be if we want to add a scale with a different value depending on the element's index. We can do this by binding the transform with a function which returns a string which contains the calculated value.

<div
	[style.transition]="'0.5s'"
	[style.transform]="isScaledDown ? getScaleDown(index) : getResetScale()"
></div>
isScaledDown = false;

getScaleDown(index: number): string {
	return `scale(${1 - (index + 1) / 10})`;
}

getResetScale(): string {
	return 'scale(1)';
}

Demo inline animation

Web Animation APIs

Another way to add animation to your application is to use regular web animation APIs (WAAPI). WAAPI at the time this was written is supported by Firefox 48+ and Chrome 36+, but it has a comprehensive and robust polyfill, making it usable in production today, even while browser support is limited. Similar to class based and inline animations, utilizing WAAPI in Angular is very similar to how regular Javascript handles it, the main difference being how we access DOM elements.

In plain HTML and Javascript we would typically give the element an id and use document.getElement.byId with the element's id to get reference to the DOM element. In Angular, we can use the template reference variable (#) instead and get it's reference by using the ViewChild decorator.

Lets first define the animation and the timing of the animation which we could use in both of our examples

getShakeAnimation() {
    return [
        { transform: 'rotate(0)' },
        { transform: 'rotate(2deg)' },
        { transform: 'rotate(-2deg)' },
        { transform: 'rotate(0)' },
    ];
}
getShakeAnimationTiming() {
    return {
        duration: 300,
        iterations: 3,
    };
}

The next two sets of snippets are how you would use the animation above in a HTML and Javascript application followed by how a slight variation of the same code can be used in an angular project.

html and js

<div id="targetElement"></div>
document
	.getElementById('targetElement')
	.animate(this.getShakeAnimation(), this.getShakeAnimationTiming());

In an angular application

<div #targeElement></div>
@ViewChild('targetElement') targetElement: ElementRef;

this.targetElement.nativeElement.animate(this.getShakeAnimation(), this.getShakeAnimationTiming());

Note that the animation part in both the snippets are exactly the same!

Demo web animations API

The web animations API also comes with some handy utility properties and functions that we can use in our Angular application the same way you would do in a regular vanilla application such as cancel to cancel the current animation and some key event listeners such as oncancel and onfinish. Here is a link to the available APIs.

Attribute Directive and Animation Builder

As described by the official documentation, an attribute directive is a means to 'change the appearance or behavior of a DOM element', making this a great way to handle a more complex animations (in terms of what triggers the animations). The nice thing about using directives is that it gives you an easy way to access the element in the DOM that the directive is applied to - letting us manipulate it the same way we would in a component, and also attach HostListeners to listen to any events and react to the emitted event.

However, unlike building out a custom component with the animations and reusing the components, a directive lets you attach just the behavior to any element in your application. It makes it more flexible if we want to reuse the same animation across different elements or components.

Directives don't have the animations array as part of the decorator, so we will have to use angular's AnimationBuilder to build the animation and AnimationPlayer to play the animation. Here is an example of an animation fading out the element on mouse down and fading it back in on mouse up.

import { Directive, HostListener, ElementRef } from '@angular/core';
import {
AnimationBuilder,
AnimationMetadata,
style,
animate,
} from '@angular/animations';

@Directive({
	selector: '[appfadeMouseDown]',
})
export class FadeMouseDownDirective {

    @HostListener('mousedown') mouseDown() {
	    this.playAnimation(this.getFadeOutAnimation());
    }
    @HostListener('mouseup') mouseUp() {
    	this.playAnimation(this.getFadeInAnimation());
    }
    
    constructor(private builder: AnimationBuilder, private el: ElementRef) {}
    
    private playAnimation(animationMetaData: AnimationMetadata[]): void {
        const animation = this.builder.build(animationMetaData);
        const player = animation.create(this.el.nativeElement);
        player.play();
    }
    
    private getFadeInAnimation(): AnimationMetadata[] {
    	return [animate('400ms ease-in', style({ opacity: 1 }))];
    }
    
    private getFadeOutAnimation(): AnimationMetadata[] {
    	return [animate('400ms ease-in', style({ opacity: 0.5 }))];
    }
}

You could also respond to keyboard events such as specific key presses by replacing what events your HostListener is listening to. For example, @HostListener('document:keydown.escape', ['$event']) would get triggered when you press on the escape key.

Now that we have the directive build out, we can use it by adding it's selector to the element in our template like this:

<div appfadeMouseDown>
    ...
</div>

Demo using attribute directive to trigger animations

Directives also let you pass in custom inputs so we could add some parameters from the component that uses the directives to set some configuration or toggle the state. I won't go into much detail on that since the official documentation has a lot of great examples on how that works.

Closing Thoughts

Making an animation work well in your application whether in the context of Angular or not, oftentimes goes beyond just writing the code. The process usually includes a lot of tweaks and refining, debugging and profiling, and testing it out on multiple different browsers for consistency and performance.

Hopefully what I covered in this post helps give you some more insight on what goes on behind the scenes when you write animation code and also provide a few more tools to help create beautiful and performant animations.

Animations are a great addition to your application, making it more interactive and fun to use, and Angular gives you a powerful tool out of the box to create beautiful and complex animations, each having its individual pro and cons and use cases. With that said, I want to end this post with a some things I try to remind myself every time I add animations to my application:

  • should be simple and quick
  • have a purpose such as guiding the user or conveying some kind of message
  • not a distraction or a barrier to be able to use your application quickly