Do you want to improve the user experience for your angular app?
To hook your user's attention, improve their understanding of how things work, and provide context to elements?
Well, good news. You can achieve all this and more, simply by adding in animations.
They don't even need to be anything too fancy. All you need are simple animations to help the users get contextual clues or feedback on their actions, and in today's article, we're going to walk you through 2 angular animation examples and how to implement them.
So let's get started...
So what method should we be using to add these animations in our app?
Well, the most common approach is to use CSS transitions but the thing is, traditional CSS isn't always enough.
Sure, it does the job pretty darn well, but it can be quite difficult to use for more complex cases, such as animating elements that must be toggled from the document.
Luckily, Angular has a package called @angular/animations that allows us to leverage JavaScript instead for performing animations in an app.
And I know what you might be thinking: "But wait... can Javascript give us the performance we need?"
It sure can so let me explain...
For the longest time, CSS was the most performant option for adding animations on the web and JavaScript couldn't compete.
However, most browsers have recently introduced the Web Animations API for performing animations with JavaScript.
What does this mean for Angular Developers?
Well under the hood, the Web Animations API uses the same engine as the CSS counterpart. And because Angular uses the same Web Animations API for handling animations, this means that we can expect the same performance as we would with plain CSS but without the drawbacks.
Sounds good?
Alright, so let's walk you through how to use this package.
Let's start by adding animations to our project.
As with any package, a module must be imported into our application to begin using its features. Animations can be imported by importing the BrowserAnimationsModule
from the @angular/platform-browser/animations package.
As a side note, I've prepared a project that you can check out from this GitHub repo. After downloading this project, you will need to install the dependencies with the npm i
command and start the server with the npm run start
command.
For this lesson, we're importing this module into the AppModule
, and you can import this like so:
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
imports: [BrowserAnimationsModule]
})
export class AppModule { }
Next, in the app.component.ts file, we can start importing a series of functions from the @angular/animations package to start performing animations.
import {
trigger, state, style, animate, transition, query, group
} from '@angular/animations';
If you're following along and implementing as you read, you might be freaking out right now because we just added a lot of functions, but don't worry because we'll go through them throughout this post.
For now we're just importing them ahead of time, so we don't have to revisit the import statement later.
The first thing to work on for any animation, regardless of the complexity, is to begin defining your animations from within a component's configuration.
Inside the @Component
decorator, you can add the animations
property to define your animations.
If you need a refresher on decorators, check out the Writing Decorators lecture from the Complete Angular Developer course.
@Component({
animations: [
trigger('fade', [])
]
})
This property is an array of animations that can be applied to the elements within the respective component's template, and a new animation can be registered by using the trigger()
function.
However, two arguments are required by this function for it to work:
The name can be whatever you want but I recommend keeping it short yet descriptive.
With the naming completed, let's have a look at each of these metadata and how they work.
When you think about it, animations can have a lot of requirements:
Answering these questions will affect the outcome of the animation, and so for this reason, Angular exports dozens of functions for handling various scenarios.
These functions will create the metadata necessary for helping Angular understand the animations. The most common functions are transition()
, style()
, and animate()
, so let's break them down.
The first type of metadata is transition metadata, which can be created with the transition()
function.
trigger('fade', [
transition(':enter', [])
])
The transition()
function describes when an animation should begin playing.
An expression can be passed as the first argument, and this particular expression will act as the condition for when an animation can start.
In this example, Angular provides an alias for elements entering the document (via the ngIf
or ngFor
directives) called :enter
.
For a complete list of possible expressions, check out this documentation link.
The second argument is an array of metadata that can be applied to the element if the animation should begin playing.
Before getting into animations, let's add another transition()
for when an element is leaving the document like so:
trigger('fade', [
transition(':enter', []),
transition(':leave', [])
])
Note:
It's completely acceptable to add multiple animations for different conditions. In this example, we will add a fade animation for when an element is entering or leaving the document.
Style metadata will describe the CSS styles that should be applied to the element. Like any animation, you will want to apply styles to an element before, during, and after the animation. In all these events, you will be able to do so by using the style()
function.
transition(':enter', [
style({ opacity: 0 })
]),
transition(':leave', [
style({ opacity: 1 }),
])
We can set the initial styles of an element by passing this function into the array. This function accepts an object of CSS properties to apply to an element.
An animation step for an element can also be added by using the animate()
function.
This function has two arguments: the duration (milliseconds) and styles to transition to.
transition(':enter', [
style({ opacity: 0 }),
animate(1000, style({ opacity: 1 }))
]),
transition(':leave', [
style({ opacity: 1 }),
animate(1000, style({ opacity: 0 }))
])
In the above example, we're setting the initial opacity of an element to 0
and animating the opacity to 1
, with a duration of 1000ms.
Then, we've reversed the effect for elements leaving the document.
Overall, our animation definition looks like the following:
@Component({
animations: [
trigger('fade', [
transition(':enter', [
style({ opacity: 0 }),
animate(1000, style({ opacity: 1 }))
]),
transition(':leave', [
style({ opacity: 1 }),
animate(1000, style({ opacity: 0 }))
])
])
]
})
After defining an animation, we can begin applying it to our elements by using the following syntax @<AnimationName>
.
For example, let's imagine we had an array of events in our component:
events = [
{
time: '08:00 - 09:00 AM',
action: 'Eat Breakfast',
bgColor: 'bg-amber-100'
},
{
time: '10:00 - 11:00 AM',
action: 'Buy Groceries',
bgColor: 'bg-teal-100'
},
{
time: '01:00 - 02:00 PM',
action: 'Workout',
bgColor: 'bg-indigo-100'
},
{
time: '03:00 - 04:00 PM',
action: 'Release Event',
bgColor: 'bg-lime-100'
}
]
Next, let's create a property for toggling elements from within the component.
showEvents = true
Lastly, we can create the UI for adding the animation:
<div class="flex justify-between border-b border-b-slate-200 min-h-[400px]">
<div class="bg-violet-50 flex justify-center flex-col p-8">
<h1 class="relative mb-4 text-4xl h-12">
<span class="absolute top-0.5 left-0.5 text-rose-400 z-10">Fade</span>
<span class="absolute z-20">Fade</span>
</h1>
<button type="button" class="py-2 px-8 first-letter:mb-2 text-white bg-lime-500"
(click)="showEvents = !showEvents">Toggle</button>
</div>
<div class="p-8 w-full flex justify-center flex-col">
<ng-container *ngIf="showEvents">
<div
*ngFor="let e of events"
[ngClass]="e.bgColor"
class="item p-4 rounded-xl w-full mb-4"
@fade
>
<div class="text-sm">{{ e.time }}</div>
<div class="font-bold">{{ e.action }}</div>
</div>
</ng-container>
</div>
</div>
The above example is using Tailwind for styling the elements, and inside the component's template, we are looping through the events
property.
Most importantly, on the same element that's being looped through, we've applied the fade
animation.
You can try testing the animation by clicking on the button that will toggle the element's appearance.
There's one problem with this animation, and it's that you can rapidly click on the button for toggling the animation, which causes the elements to restart and appear multiple times.
Not great so how do we solve this?
Well, we can wait for the animation to be complete before triggering the animation again by using events.
All animations have two events called start
and end
, which will be called when an animation starts and ends, respectively.
First, let's update our component's class to store a property for storing the current status of the animation, along with a method for checking this property before toggling the elements.
toggleFlag = false
toggleEvents() {
if(!this.toggleFlag) {
this.showEvents = !this.showEvents
}
}
Next, we can update the click
event on the button to use the toggleEvents()
method.
<button type="button" class="py-2 px-8 first-letter:mb-2 text-white bg-lime-500" (click)="showEvents = !showEvents">Toggle</button>
Lastly, we can update the element with the animation by adding the start
and end
events to toggle the toggleFlag
property.
<div
*ngFor="let e of events"
[ngClass]="e.bgColor"
class="item p-4 rounded-xl w-full mb-4"
@fade
(@fade.start)="toggleFlag = true"
(@fade.done)="toggleFlag = false"
>
...
</div>
After making these changes, the animations should not overlap one another. Instead, they will wait for an animation to finish before starting another.
State metadata describes the final state of an element after an animation is finished playing. While not required, we can describe the element's appearance for different states, and this feature can make our code more readable.
State metadata can be created with the state()
function.
The state()
method has two arguments, which are the name of the state and the styles to apply to the element.
Here's an example.
trigger('fade', [
state('void', style({ opacity: 0 })),
state('*', style({ opacity: 1 })),
transition(':enter, :leave', [
animate(1000)
])
])
We've completely changed the fade
animation's metadata, but the result will be the same.
Let's break down the changes:
void
and *
. The void
state describes the element after it's been removed from the document. This is the official name of the state given by Angular to elements that are not in the document. The *
state describes an element after it's been added to the document. Also known as the default state.void
state will have an opacity of 0
, whereas the *
state will have an opacity of 1
.transition()
function has been updated to include both the :enter
and :leave
transitions. Inside this function, we are adding the animate()
function without adding the styles. Angular will be intelligent enough to refer to our states for the styles that should be applied to the element based on the action.Using the state()
function is optional, but it can be a great way to reduce the amount of code you have to write for an animation.
Animations can also be tailored to different elements, so let's look at a more complex example called fadeSlide
.
trigger('fadeSlide', [
transition(':enter', [
query('.item:nth-child(odd)', [
style({ opacity: 0, transform: 'translateX(-250px)' }),
animate(
1000,
style({ opacity: 1, transform: 'translateX(0)' })
)
]),
query('.item:nth-child(even)', [
style({ opacity: 0, transform: 'translateX(250px)' }),
animate(
1000,
style({ opacity: 1, transform: 'translateX(0)' })
)
])
]),
transition(':leave', [
query('.item:nth-child(odd)', [
animate(
1000,
style({ opacity: 0, transform: 'translateX(-250px)' })
)
]),
query('.item:nth-child(even)', [
animate(
1000,
style({ opacity: 0, transform: 'translateX(250px)' })
),
])
])
])
This animation will slide an element to the left or right based on a query, and Query metadata will allow us to select specific elements from within the current animation.
Note:
We don't have to always have apply an animation directly to the element, as we can perform a query with the query()
function instead.
This function has two arguments:
In this example, we're applying animations to odd and even elements. Odd elements will slide left, while even elements will slide right.
Group metadata allows us to group animations so that they're played simultaneously, instead of the default which is to be played one after another.
Right now this current animation will run in a sequence where the first element will slide left before the next element starts and slides right.
We want them to run all at the same time, and so to avoid this issue, we can play both animations simultaneously by grouping them with the group()
function like so:
trigger('fadeSlide', [
transition(':enter', [
group([
query('.item:nth-child(odd)', [
style({ opacity: 0, transform: 'translateX(-250px)' }),
animate(
1000,
style({ opacity: 1, transform: 'translateX(0)' })
)
]),
query('.item:nth-child(even)', [
style({ opacity: 0, transform: 'translateX(250px)' }),
animate(
1000,
style({ opacity: 1, transform: 'translateX(0)' })
)
])
])
]),
transition(':leave', [
group([
query('.item:nth-child(odd)', [
animate(
1000,
style({ opacity: 0, transform: 'translateX(-250px)' })
)
]),
query('.item:nth-child(even)', [
animate(
1000,
style({ opacity: 0, transform: 'translateX(250px)' })
),
])
])
])
])
An array of metadata can be passed into this function, and the query()
functions are wrapped with the group()
function.
After making those changes, we can apply the new grouped version by moving the animation to the parent element:
<div *ngIf="showEvents"
@fadeSlide
(@fadeSlide.start)="toggleFlag = true"
(@fadeSlide.done)="toggleFlag = false"
>
<div
*ngFor="let e of events"
[ngClass]="e.bgColor"
class="item p-4 rounded-xl w-full mb-4"
>
<div class="text-sm">{{ e.time }}</div>
<div class="font-bold">{{ e.action }}</div>
</div>
</div>
In this example, we're applying the fadeSlide
animation on the <div>
element with the ngIf
element.
But why?
Good question! In order to use our query, we must move the animation to the parent element wrapped around the loop. Otherwise, we wouldn't be able to select even and odd elements.
Pretty simple right?
The beauty of this package is it allows Angular to provide even more functions for fine tuning animations, and adding interactivity to an application is never a bad idea. As we shared in the introduction, even simple animations can help boost user experience!
Hopefully, this post gave you insight into how you can get started with your own Animations in Angular.
If you want to learn even more animations then read this guide from Angular to learn more, or you can check out my Complete Angular Developer course for 2022 for a guided walkthrough with more examples, and in-depth training on other some of the other important elements of Angular.