Overview
Angular’s animation system provides a powerful DSL (Domain Specific Language) for creating complex animations and transitions. Built on top of the Web Animations API, it offers declarative syntax for common animation patterns.Setup
Import the animations module to enable animation support.Standalone Application
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideAnimations()
]
});
NgModule Application
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';
@NgModule({
imports: [
BrowserAnimationsModule
]
})
export class AppModule {}
Basic Animations
Simple Trigger
Create basic animations using triggers and states.import { Component } from '@angular/core';
import {
trigger,
state,
style,
transition,
animate
} from '@angular/animations';
@Component({
selector: 'app-toggle',
template: `
<div class="box" [@openClose]="isOpen ? 'open' : 'closed'">
<p>This box animates!</p>
</div>
<button (click)="toggle()">Toggle</button>
`,
animations: [
trigger('openClose', [
state('open', style({
height: '200px',
opacity: 1,
backgroundColor: '#4CAF50'
})),
state('closed', style({
height: '100px',
opacity: 0.8,
backgroundColor: '#F44336'
})),
transition('open => closed', [
animate('0.5s')
]),
transition('closed => open', [
animate('0.3s')
])
])
]
})
export class ToggleComponent {
isOpen = false;
toggle(): void {
this.isOpen = !this.isOpen;
}
}
Wildcard States
Use wildcards for flexible state matching.import { Component } from '@angular/core';
import { trigger, state, style, transition, animate } from '@angular/animations';
@Component({
selector: 'app-status',
template: `
<div class="status" [@statusChange]="currentStatus">
Status: {{ currentStatus }}
</div>
<button (click)="setStatus('active')">Active</button>
<button (click)="setStatus('inactive')">Inactive</button>
<button (click)="setStatus('pending')">Pending</button>
`,
animations: [
trigger('statusChange', [
state('active', style({ backgroundColor: '#4CAF50', color: 'white' })),
state('inactive', style({ backgroundColor: '#F44336', color: 'white' })),
state('pending', style({ backgroundColor: '#FF9800', color: 'white' })),
// Transition from any state to any state
transition('* => *', [
animate('300ms ease-in-out')
])
])
]
})
export class StatusComponent {
currentStatus = 'inactive';
setStatus(status: string): void {
this.currentStatus = status;
}
}
Enter and Leave Animations
Animate elements as they enter or leave the DOM.import { Component } from '@angular/core';
import {
trigger,
transition,
style,
animate
} from '@angular/animations';
@Component({
selector: 'app-list',
template: `
<button (click)="addItem()">Add Item</button>
<ul>
<li
*ngFor="let item of items; trackBy: trackByFn"
[@fadeInOut]
(click)="removeItem(item.id)"
>
{{ item.name }}
</li>
</ul>
`,
animations: [
trigger('fadeInOut', [
transition(':enter', [
style({ opacity: 0, transform: 'translateY(-20px)' }),
animate('300ms ease-out', style({
opacity: 1,
transform: 'translateY(0)'
}))
]),
transition(':leave', [
animate('300ms ease-in', style({
opacity: 0,
transform: 'translateX(100%)'
}))
])
])
]
})
export class ListComponent {
items = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
];
private nextId = 3;
addItem(): void {
this.items.push({
id: this.nextId++,
name: `Item ${this.nextId}`
});
}
removeItem(id: number): void {
this.items = this.items.filter(item => item.id !== id);
}
trackByFn(index: number, item: any): number {
return item.id;
}
}
Use
:enter and :leave aliases for the special void => * and * => void transitions.Advanced Animations
Keyframes
Create complex multi-step animations.import { Component } from '@angular/core';
import {
trigger,
transition,
style,
animate,
keyframes
} from '@angular/animations';
@Component({
selector: 'app-bounce',
template: `
<div
class="ball"
[@bounce]="animationState"
(click)="bounce()"
>
Click me!
</div>
`,
animations: [
trigger('bounce', [
transition('* => bouncing', [
animate('1s', keyframes([
style({ transform: 'translateY(0)', offset: 0 }),
style({ transform: 'translateY(-100px)', offset: 0.3 }),
style({ transform: 'translateY(0)', offset: 0.5 }),
style({ transform: 'translateY(-50px)', offset: 0.7 }),
style({ transform: 'translateY(0)', offset: 0.85 }),
style({ transform: 'translateY(-20px)', offset: 0.95 }),
style({ transform: 'translateY(0)', offset: 1 })
]))
])
])
]
})
export class BounceComponent {
animationState = 'idle';
bounce(): void {
this.animationState = 'bouncing';
setTimeout(() => this.animationState = 'idle', 1000);
}
}
Parallel Animations
Run multiple animations simultaneously usinggroup().
import { Component } from '@angular/core';
import {
trigger,
transition,
style,
animate,
group
} from '@angular/animations';
@Component({
selector: 'app-card',
template: `
<div
class="card"
[@cardFlip]="flipped ? 'flipped' : 'default'"
(click)="flip()"
>
<div class="front">Front</div>
<div class="back">Back</div>
</div>
`,
animations: [
trigger('cardFlip', [
state('default', style({
transform: 'rotateY(0)'
})),
state('flipped', style({
transform: 'rotateY(180deg)'
})),
transition('default => flipped', [
group([
animate('600ms', style({ transform: 'rotateY(180deg)' })),
animate('300ms', style({ opacity: 0.5 }))
])
]),
transition('flipped => default', [
group([
animate('600ms', style({ transform: 'rotateY(0)' })),
animate('300ms', style({ opacity: 1 }))
])
])
])
]
})
export class CardComponent {
flipped = false;
flip(): void {
this.flipped = !this.flipped;
}
}
Sequential Animations
Chain animations usingsequence().
import { Component } from '@angular/core';
import {
trigger,
transition,
style,
animate,
sequence
} from '@angular/animations';
@Component({
selector: 'app-notification',
template: `
<div
*ngIf="visible"
class="notification"
[@slideAndFade]
>
{{ message }}
</div>
`,
animations: [
trigger('slideAndFade', [
transition(':enter', [
sequence([
style({
opacity: 0,
transform: 'translateX(100%)'
}),
animate('300ms ease-out', style({
transform: 'translateX(0)'
})),
animate('200ms', style({
opacity: 1
}))
])
]),
transition(':leave', [
sequence([
animate('200ms', style({
opacity: 0
})),
animate('300ms ease-in', style({
transform: 'translateX(100%)'
}))
])
])
])
]
})
export class NotificationComponent {
visible = false;
message = '';
show(message: string, duration = 3000): void {
this.message = message;
this.visible = true;
setTimeout(() => this.visible = false, duration);
}
}
Stagger Animations
Animate list items with a stagger effect.import { Component } from '@angular/core';
import {
trigger,
transition,
style,
animate,
query,
stagger
} from '@angular/animations';
@Component({
selector: 'app-stagger-list',
template: `
<button (click)="toggle()">Toggle List</button>
<ul *ngIf="show" [@listAnimation]>
<li *ngFor="let item of items">{{ item }}</li>
</ul>
`,
animations: [
trigger('listAnimation', [
transition('* => *', [
query(':enter', [
style({
opacity: 0,
transform: 'translateY(20px)'
}),
stagger(100, [
animate('300ms ease-out', style({
opacity: 1,
transform: 'translateY(0)'
}))
])
], { optional: true })
])
])
]
})
export class StaggerListComponent {
show = false;
items = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'];
toggle(): void {
this.show = !this.show;
}
}
Route Animations
Animate transitions between routes.// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
const routes: Routes = [
{ path: '', component: HomeComponent, data: { animation: 'HomePage' } },
{ path: 'about', component: AboutComponent, data: { animation: 'AboutPage' } }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
// animations.ts
import {
trigger,
transition,
style,
query,
group,
animate
} from '@angular/animations';
export const slideInAnimation = trigger('routeAnimations', [
transition('HomePage <=> AboutPage', [
style({ position: 'relative' }),
query(':enter, :leave', [
style({
position: 'absolute',
top: 0,
left: 0,
width: '100%'
})
], { optional: true }),
query(':enter', [
style({ left: '-100%' })
], { optional: true }),
query(':leave', [
// Leave animation not needed
], { optional: true }),
group([
query(':leave', [
animate('300ms ease-out', style({ left: '100%' }))
], { optional: true }),
query(':enter', [
animate('300ms ease-out', style({ left: '0%' }))
], { optional: true })
])
])
]);
// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { slideInAnimation } from './animations';
@Component({
selector: 'app-root',
template: `
<div [@routeAnimations]="getRouteAnimationData()">
<router-outlet #outlet="outlet"></router-outlet>
</div>
`,
animations: [slideInAnimation]
})
export class AppComponent {
getRouteAnimationData() {
return this.outlet?.activatedRouteData?.['animation'];
}
}
Animation Callbacks
Listen to animation events.import { Component } from '@angular/core';
import {
trigger,
state,
style,
transition,
animate
} from '@angular/animations';
@Component({
selector: 'app-callback',
template: `
<div
[@fadeInOut]="visible ? 'visible' : 'hidden'"
(@fadeInOut.start)="onAnimationStart($event)"
(@fadeInOut.done)="onAnimationDone($event)"
>
Content
</div>
<button (click)="toggle()">Toggle</button>
`,
animations: [
trigger('fadeInOut', [
state('visible', style({ opacity: 1 })),
state('hidden', style({ opacity: 0 })),
transition('visible <=> hidden', animate('500ms'))
])
]
})
export class CallbackComponent {
visible = true;
toggle(): void {
this.visible = !this.visible;
}
onAnimationStart(event: any): void {
console.log('Animation started:', event);
}
onAnimationDone(event: any): void {
console.log('Animation completed:', event);
}
}
Reusable Animations
Create reusable animation definitions.// animations/fade.ts
import { animation, style, animate } from '@angular/animations';
export const fadeIn = animation([
style({ opacity: 0 }),
animate('{{ duration }}', style({ opacity: 1 }))
]);
export const fadeOut = animation([
animate('{{ duration }}', style({ opacity: 0 }))
]);
// component.ts
import { Component } from '@angular/core';
import {
trigger,
transition,
useAnimation
} from '@angular/animations';
import { fadeIn, fadeOut } from './animations/fade';
@Component({
selector: 'app-reusable',
template: `
<div [@fade]="show" *ngIf="show">
Reusable animation
</div>
`,
animations: [
trigger('fade', [
transition(':enter', [
useAnimation(fadeIn, {
params: { duration: '300ms' }
})
]),
transition(':leave', [
useAnimation(fadeOut, {
params: { duration: '200ms' }
})
])
])
]
})
export class ReusableComponent {
show = true;
}
Best Practices
Use Hardware Acceleration
Animate
transform and opacity for better performance.Provide Animation Metadata
Use the
optional flag in queries for flexible animations.Disable in Tests
Use
provideNoopAnimations() in tests for faster execution.Reuse Definitions
Create reusable animations with
animation() and useAnimation().Avoid animating properties like
width, height, or left directly as they trigger layout recalculation. Use transform: scale() and transform: translate() instead.