Animations in Angular

Sergey Gultyayev
8 min readFeb 8, 2023

--

When you come to Angular first you already know HTML, CSS and JS and hence if you need to implement animations the first thing coming to mind is CSS transitions and animations using keyframes. In Angular we have another way to animate stuff — by using BrowserAnimationsModule provided by Angular.

Photo by Zhifei Zhou on Unsplash

The problem with native CSS animations

Despite these are performant and familiar to us, they are not sufficient for complex use cases such as changing width and height to “auto” value, chaining animations, reversing animation in the middle state (such as closing an accordion while it’s still expanding) etc.

In the old days to handle complex animations we used to use jQuery, then (ideally) to use requestAnimationFrame to do non-blocking animations, however these are much harder to implement. Then we could use some libraries.

However, there is a huge problem with the approaches above performance. When you are changing values via JS you first calculate current elements properties (in case you need to get back to them or use the values for reference), then apply the animations. All of this happens synchronously and in one thread which is not ideal and is a huge performance hit.

To fix the problem, while allowing developers to play animations, reverse, play animations manually step by step etc. Web Animations API were introduced. They allow us to create performant animations as they work the same way regular CSS animations do — in a separate thread and in an optimized way. What’s more exciting is that Angular’s animations are using Web Animations API! Therefore animations you write in Angular are performant by default.

Working with Angular animations

At first, animations in Angular look too complicated and hard to digest, but after you grasp the basics it doesn’t look that intimidating. I do acknowledge that they aren’t easy to read through, but this is a consequence of making them versatile and allowing combining animations.

Angular animations are not a replacement for CSS animations, but an addition to them. If you can animate through CSS transition or keyframes it’s better to do it so. However, even if it’s a simple animation, but it needs to animate when an element updates or similar case which would require you to manually apply and then remove a class to animate it once again later — this is when you want to switch to Angular animations.

Let’s have a look at examples.

Animating list of cards

Here, we animate each card item in the list when they enter the screen with flyIn animation. When an element is clicked on, it fades out and removed. Imagine doing this animation with standard CSS or in React. Animating the element that leaves the DOM is not a straightforward thing to implement. In Angular it’s just a breeze. You simply define query(':leave', …) and Angular will do the animation upon removal for you!

trigger('flyIn', [
transition('* => *', [
query(':enter', [
style({
transform: 'translateX(20px) translateY(20px) rotateZ(20deg)',
opacity: 0,
}),
stagger(100, [
animate(
'200ms',
style({
transform: 'none',
opacity: 1,
})
),
]),
], {
optional: true
}),
query(':leave', [
animate(200, style({
opacity: 0
}))
], {optional: true})
]),
])

Now that we know what are animating, let’s understand how:

  1. We import BrowserAnimationsModule in AppModule‘s imports.
  2. We add animations property in the Component’s metadata. It accepts an array of triggers.
  3. Specify a trigger('flyIn', [...]) — a function which accepts 2 arguments: a name of a trigger (any name you like to assign on DOM elements via @triggerName , in our case flyIn ); array of transitions.
  4. Define a transition('* => *', [...]) . * is a wildcard and is used to reference any state. * => * means that whenever a bound value of [@flyIn] is changed we want to perform our animation.
  5. We query for elements that :enter (those that are added by Angular and are children of the DOM element we assigned our trigger to).
  6. As a second argument we specify an array that will define our animation:
    1. style is an initial state for the DOM element. It’s an object of CSS styles.
    2. stagger(100, [...]) tells Angular to animate all queried elements delaying each sibling animation by n * 100
    3. Within stagger‘s array we animate the state change for 200ms to the state of style specified next to the time. The transition between the initial and final styles is handled by the browser.
  7. As a third argument for query we specify { optional: true } this is done for the case when we have no elements returned by the query so it doesn’t throw an error.
  8. We query for elements that :leave (those that are removed from DOM by Angular and are children of the DOM element we assigned our trigger to).
  9. We animate the element from its current state (by not specifying any style).
  10. We assign [@flyIn]="users.length" to the container of app-cards. We use users.length in order to trigger the animation for each addition and removal of the items in the list. When the value bound to [@flyIn] changes Angular will try to find a transition that matches the state change and then execute its animation if found.

Animate element’s width & height to its auto size

As you know you cannot animate element’s size to auto value, however in Angular you can specify the size as * and it will be working! No need to write workarounds to calculate element’s size somehow before rendering it to the user.

trigger('accordion', [
transition(':enter', [
style({
height: 0
}),
animate(200, style({
height: '*'
}))
]),
transition(':leave', [
animate(100, style({
height: 0
}))
])
])

Here, we define the trigger on a div via @accordion with no value bound. We are not changing any states and only need the trigger to use :enter, :leave transitions. For :enter transition we specify an initial style with height: 0 and animate it to * — the element’s height calculated by the browser. For :leave transition we only specify animation to the height: 0 as we want to use current element’s height.

Animate popup

Oftentimes we need something more complex than just going from state A to state B. We might want to add a state C in the chain, might want to make transition to states B and C simultaneous. For this purpose we have functions such as:

  • group to run several animations in parallel
  • sequence to run animations (as you have guessed) in sequence

In the example I intentionally make the popup’s borders visible so it can be clearly observed how it first expands it width to the“auto” value and then expand in height while also transitioning the opacity from 0 to 1.

trigger('popupExpanded', [
state(
'false',
style({
height: 20,
width: 20,
})
),
transition('false => true', [
sequence([
animate(200, style({
width: '*'
})),
group([
animate(200, style({
height: '*'
})),
query('.popup-content', [
style({
opacity: 0
}),
animate(200, style({
opacity: 1
}))
])
])
])
]),
transition('true => false', [
sequence([
group([
animate(200, style({
height: 20
})),
query('.popup-content', [
style({
opacity: 1
}),
animate(200, style({
opacity: 0
}))
])
]),
animate(200, style({
width: 20
}))
])
])
])
  1. We declare state false for when the popup is collapsed and set its height and width to 20px
  2. We declare transition from false to true using sequence
  3. Sequence consists of 2 steps:
    a) changing popup’s width
    b) changing popup’s height and changing opacity of its content from 0 to 1
  4. 3.b is implemented by using group which allows us to parallelize two separate animations
  5. In order to animate popup’s content opacity we use query with .popup-content CSS selector where we define its initial state and animation
  6. We reverse the steps for true => false transition so that it first changes height and opacity and only then collapse its width

We could extract all trigger function call and its contents into a separate file and export it as a const. Thus we would have a reusable animation which doesn’t create clutter in the component’s body.

Taking the matter in our own hands

While all those functions and all are fancy, but what if we want to do some advanced scenarios? Let’s say animate elements when performing gestures, or tie animations to a progress bar.

For this we would need to use AnimationBuilder . We build the animation itself the same way we would do it previously. We don’t need to create a trigger because we are the ones responsible for attaching the animation to an element and then we are in full control of it.

I have extracted the animation into a separate constant just to show that we store it out of the component and use it later at our convenience. In the example below we have a box which animation is tied to the range (0 to 100). This way when the range input is at value 0 the box is at its initial state. When we move the range to the 100 the box transitions to its final state. While changing the input’s value we can see that box is following the values change. Let’s break it down.

const animation = [
style({
tansform: 'translateX(0)',
}),
animate(
'200ms ease-in-out',
style({
transform: 'translateX(100%)',
})
),
];
// app.component.ts
ngOnInit() {
this.player = this.animationBuilder
.build(animation)
.create(this.boxEl.nativeElement);

this.offset.valueChanges.subscribe((value) => {
this.player.setPosition(value / 100);
});
}
  1. As always we define some animation. Since it’s a trivial one I won’t be going into the details of what the animation is supposed to do
  2. We inject an AnimationBuilder instance
  3. We get a reference to the box by using ViewChild and specifying static: true so that it can be accessed right in the ngOnInit hook
  4. We build an animation using this.animationBuilder.build(animation) which returns an AnimationFactory . It has the only method create which accepts a DOM element which will be animated
  5. We chain create directly to the build as I’m not going to attach one animation to many elements
  6. create returns an AnimationPlayer this is the object that gives us full control over the animation
  7. We bind the range input to a FormControl so to subscribe to its valueChanges and call this.player.setPosition() on each value change
  8. We set a position using value from 0 to 1, where 0 means the element is in its initial state and 1 — final

AnimationPlayer presents us with a ton of useful methods, so its worth reading through the docs to know what Angular can give us.

As you can see we can put an element into an intermediate state whenever we want. This way it’s easy to connect such animation to gestures or anything else where you can get numbers that would result in the 0…1 position for the animation timeline.

Tip: you can change easing in the animate() and this will affect the manual animation as well.

Conclusion

Angular is shipped with a great animation engine which works on top of the native Web Animations API. We want to use it when we need to animate transitions between states or when we want to gain manual control over the animations using Angular’s AnimationBuilder .

For simple animations we still would want to use CSS as it keeps the code much cleaner and eliminates the overhead of using JS to delegate animation tasks to the browser.

--

--

Sergey Gultyayev
Sergey Gultyayev

Written by Sergey Gultyayev

A front-end developer who uses Angular as a main framework and loves it

No responses yet