Animations in Angular
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.
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:
- We import
BrowserAnimationsModule
inAppModule
‘s imports. - We add
animations
property in the Component’s metadata. It accepts an array oftrigger
s. - 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 caseflyIn
); array of transitions. - 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. - We
query
for elements that:enter
(those that are added by Angular and are children of the DOM element we assigned our trigger to). - 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 byn * 100
3. Withinstagger
‘s array weanimate
the state change for 200ms to the state ofstyle
specified next to the time. The transition between the initial and final styles is handled by the browser. - As a third argument for
query
we specify{ optional: true }
this is done for the case when we have no elements returned by thequery
so it doesn’t throw an error. - 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). - We
animate
the element from its current state (by not specifying any style). - We assign
[@flyIn]="users.length"
to the container ofapp-card
s. We useusers.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 atransition
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 parallelsequence
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
}))
])
])
])
- We declare state
false
for when the popup is collapsed and set its height and width to 20px - We declare transition from
false
totrue
usingsequence
- 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 - 3.b is implemented by using
group
which allows us to parallelize two separate animations - In order to animate popup’s content opacity we use
query
with.popup-content
CSS selector where we define its initial state and animation - 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);
});
}
- 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
- We inject an
AnimationBuilder
instance - We get a reference to the box by using
ViewChild
and specifyingstatic: true
so that it can be accessed right in thengOnInit
hook - We build an animation using
this.animationBuilder.build(animation)
which returns anAnimationFactory
. It has the only methodcreate
which accepts a DOM element which will be animated - We chain
create
directly to thebuild
as I’m not going to attach one animation to many elements create
returns anAnimationPlayer
this is the object that gives us full control over the animation- We bind the range input to a FormControl so to subscribe to its
valueChanges
and callthis.player.setPosition()
on each value change - 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.