Angular Animation
Introduction
Animations are an essential part of modern web applications, helping to create a more engaging and intuitive user experience. They guide users through navigation, provide feedback on interactions, and add a layer of polish to your application. Angular provides a powerful animation system built on top of the Web Animations API, making it easy to create complex animations with minimal code.
In this tutorial, we'll explore Angular's animation system, understand its core concepts, and learn how to implement various types of animations in your Angular applications.
Setting Up Angular Animations
Before we can use Angular animations, we need to set up our project properly.
Step 1: Import the Animation Module
First, we need to import the BrowserAnimationsModule
in our application's root module:
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
imports: [
BrowserAnimationsModule,
// other imports...
],
// ...
})
export class AppModule { }
Step 2: Import Animation Functions
Next, we need to import the specific animation functions we'll be using in our component:
import {
trigger,
state,
style,
animate,
transition
} from '@angular/animations';
Core Animation Concepts
Angular's animation system is built around several key concepts:
1. Triggers
Triggers are what link your animations to your template. They have a name and contain states and transitions.
2. States
States define the styles at the beginning or end of animations.
3. Transitions
Transitions define how elements animate from one state to another.
4. Timing
Timing controls how long animations take and how they accelerate/decelerate.
Basic Animation Example
Let's start with a simple example that toggles an element's visibility with a fade effect:
@Component({
selector: 'app-fade-example',
template: `
<button (click)="isVisible = !isVisible">Toggle</button>
<div [@fadeAnimation]="isVisible ? 'visible' : 'hidden'">
This content will fade in and out
</div>
`,
animations: [
trigger('fadeAnimation', [
state('hidden', style({
opacity: 0
})),
state('visible', style({
opacity: 1
})),
transition('hidden => visible', [
animate('0.3s')
]),
transition('visible => hidden', [
animate('0.3s')
])
])
]
})
export class FadeExampleComponent {
isVisible = true;
}
In this example:
- We've defined a trigger called
fadeAnimation
- We've created two states:
hidden
(opacity: 0) andvisible
(opacity: 1) - We've defined transitions between these states with a 0.3 second animation
- We bind the trigger to an element in the template and control the state with a boolean property
Animation Timing
Animation timing can be controlled with the animate()
function. The first parameter is a timing string with the following format:
duration [delay] [easing]
For example:
animate('1s')
- 1 second durationanimate('1s 0.5s')
- 1 second duration with a 0.5 second delayanimate('1s ease-in')
- 1 second duration with ease-in easinganimate('1s 0.5s ease-in-out')
- 1 second duration with a 0.5 second delay and ease-in-out easing
Let's modify our previous example to include different timing:
transition('hidden => visible', [
animate('0.5s 0.2s ease-in')
]),
transition('visible => hidden', [
animate('0.3s ease-out')
])
Multi-Step Animations
We can create more complex animations by using multiple steps:
@Component({
selector: 'app-multi-step',
template: `
<button (click)="state = state === 'normal' ? 'highlighted' : 'normal'">
Toggle State
</button>
<div [@multiStep]="state" class="box">
Multi-step animation
</div>
`,
styles: [`
.box {
width: 100px;
height: 100px;
border: 1px solid black;
padding: 10px;
}
`],
animations: [
trigger('multiStep', [
state('normal', style({
backgroundColor: 'white',
transform: 'scale(1)'
})),
state('highlighted', style({
backgroundColor: 'lightblue',
transform: 'scale(1.1)'
})),
transition('normal => highlighted', [
style({ backgroundColor: 'white' }),
animate('0.2s', style({ backgroundColor: 'yellow' })),
animate('0.3s', style({ backgroundColor: 'lightblue', transform: 'scale(1.1)' }))
]),
transition('highlighted => normal', [
animate('0.5s')
])
])
]
})
export class MultiStepComponent {
state = 'normal';
}
In this example, the transition from normal
to highlighted
happens in multiple steps:
- Start with a white background
- Animate to a yellow background over 0.2 seconds
- Then animate to a light blue background and scale up over 0.3 seconds
Wildcard State and Void State
Angular animations provide special states for more flexible transitions:
*
(wildcard) - matches any statevoid
- represents when an element is not part of the DOM
These are useful for entering (adding to DOM) and leaving (removing from DOM) animations:
@Component({
selector: 'app-enter-leave',
template: `
<button (click)="items.push(items.length + 1)">Add</button>
<button (click)="items.pop()">Remove</button>
<div *ngFor="let item of items" [@enterLeave]>
Item {{ item }}
</div>
`,
animations: [
trigger('enterLeave', [
transition(':enter', [
style({ opacity: 0, transform: 'translateY(-20px)' }),
animate('0.3s ease-out', style({ opacity: 1, transform: 'translateY(0)' }))
]),
transition(':leave', [
animate('0.3s ease-in', style({ opacity: 0, transform: 'translateY(20px)' }))
])
])
]
})
export class EnterLeaveComponent {
items: number[] = [1, 2, 3];
}
In this example:
:enter
is a shorthand forvoid => *
(element entering the DOM):leave
is a shorthand for* => void
(element leaving the DOM)
Query and Stagger for Group Animations
For more complex scenarios, we can use query()
to find elements and stagger()
to create sequential animations:
@Component({
selector: 'app-list-animation',
template: `
<button (click)="toggle()">Toggle</button>
<div [@listAnimation]="state">
<div *ngFor="let item of items" class="list-item">
Item {{ item }}
</div>
</div>
`,
styles: [`
.list-item {
padding: 10px;
border-bottom: 1px solid #ddd;
width: 200px;
}
`],
animations: [
trigger('listAnimation', [
transition('* => visible', [
query('.list-item', style({ opacity: 0, transform: 'translateY(-20px)' })),
query('.list-item', stagger('100ms', [
animate('0.3s', style({ opacity: 1, transform: 'translateY(0)' }))
]))
]),
transition('visible => *', [
query('.list-item', stagger('100ms', [
animate('0.3s', style({ opacity: 0, transform: 'translateY(20px)' }))
]))
])
])
]
})
export class ListAnimationComponent {
state = 'visible';
items = [1, 2, 3, 4, 5];
toggle() {
this.state = this.state === 'visible' ? 'hidden' : 'visible';
}
}
In this example:
- We use
query()
to select all elements with the class.list-item
- We use
stagger()
to add a delay between each item's animation - This creates a cascading effect where each item animates one after the other
Route Transitions
One of the most common use cases for animations is transitions between different routes. Here's a basic example of how to implement route animations:
// app.component.ts
@Component({
selector: 'app-root',
template: `
<div class="page-container">
<div [@routeAnimations]="prepareRoute(outlet)">
<router-outlet #outlet="outlet"></router-outlet>
</div>
</div>
`,
animations: [
trigger('routeAnimations', [
transition('* => *', [
style({ position: 'relative' }),
query(':enter, :leave', [
style({
position: 'absolute',
top: 0,
left: 0,
width: '100%'
})
], { optional: true }),
query(':enter', [
style({ opacity: 0 })
], { optional: true }),
query(':leave', [
animate('300ms ease-out', style({ opacity: 0 }))
], { optional: true }),
query(':enter', [
animate('300ms ease-out', style({ opacity: 1 }))
], { optional: true })
])
])
]
})
export class AppComponent {
prepareRoute(outlet: RouterOutlet) {
return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation'];
}
}
For this to work, you need to define animation data in your routes:
// app-routing.module.ts
const routes: Routes = [
{
path: 'home',
component: HomeComponent,
data: { animation: 'HomePage' }
},
{
path: 'about',
component: AboutComponent,
data: { animation: 'AboutPage' }
},
// other routes...
];
Real-World Example: Expandable Panel
Let's create a practical component that users can expand and collapse with a smooth animation:
@Component({
selector: 'app-expandable-panel',
template: `
<div class="panel">
<div class="panel-header" (click)="toggle()">
<h3>{{ title }}</h3>
<span class="icon" [@rotateIcon]="isExpanded ? 'expanded' : 'collapsed'">▼</span>
</div>
<div class="panel-content" [@expandCollapse]="isExpanded ? 'expanded' : 'collapsed'">
<ng-content></ng-content>
</div>
</div>
`,
styles: [`
.panel {
width: 100%;
max-width: 500px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 10px;
overflow: hidden;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background-color: #f5f5f5;
cursor: pointer;
}
.panel-content {
padding: 0 15px;
}
.icon {
font-size: 12px;
}
`],
animations: [
trigger('expandCollapse', [
state('collapsed', style({
height: '0',
paddingTop: '0',
paddingBottom: '0',
opacity: 0
})),
state('expanded', style({
height: '*',
padding: '15px',
opacity: 1
})),
transition('collapsed <=> expanded', [
animate('0.3s ease-out')
])
]),
trigger('rotateIcon', [
state('collapsed', style({ transform: 'rotate(0)' })),
state('expanded', style({ transform: 'rotate(180deg)' })),
transition('collapsed <=> expanded', [
animate('0.3s')
])
])
]
})
export class ExpandablePanelComponent {
@Input() title: string = 'Panel Title';
isExpanded: boolean = false;
toggle() {
this.isExpanded = !this.isExpanded;
}
}
Usage of this component would look like:
<app-expandable-panel title="Important Information">
<p>This is the content that will be shown when the panel is expanded.</p>
<p>You can put any content here!</p>
</app-expandable-panel>
Performance Considerations
When working with animations, keep these performance tips in mind:
-
Animation Properties: Animate properties that are cheap for browsers to animate, like
opacity
andtransform
rather thanwidth
orheight
-
Will-Change Property: For complex animations, consider adding the
will-change
CSS property to hint to the browser about upcoming changes:css.animated-element {
will-change: transform, opacity;
} -
Limit the Number of Animated Elements: Animating many elements simultaneously can cause performance issues
-
Disable Animations: Consider providing an option to disable animations for users who prefer reduced motion:
typescript// Check prefers-reduced-motion media query
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
Summary
Angular's animation system provides a powerful and flexible way to add motion and interactivity to your applications. In this tutorial, we've covered:
- Setting up Angular animations
- Basic transitions between states
- Timing and easing functions
- Multi-step animations
- Enter and leave animations
- Staggered animations for lists
- Route transitions
- A practical expandable panel component
With these tools in your arsenal, you can create engaging user experiences that guide user attention, provide feedback, and make your applications feel polished and professional.
Additional Resources and Exercises
Resources:
- Official Angular Animation Documentation
- Angular Animation Examples on GitHub
- Web Animations API Documentation
Exercises:
-
Toggle Button: Create an animated toggle button that smoothly transitions between on/off states.
-
Notification System: Implement a notification component that slides in from the top, stays for a few seconds, then slides out.
-
Image Carousel: Build a simple image carousel with smooth animations between slides.
-
Loading Indicator: Create a custom loading spinner with continuous animation.
-
Accordion Menu: Extend the expandable panel example to create a full accordion where opening one panel closes the others, all with smooth animations.
Happy animating!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)