Angular Animations
Angular has a built-in animation system powered by the Web Animations API. It lets you define multi-step transitions in component metadata using a declarative DSL — no raw CSS keyframes or JavaScript timers needed.
Angular animations are especially good at:
- Enter/leave transitions (
*ngIf/@iftoggles) - State-based animations (expanded/collapsed, active/inactive)
- List animations (items entering and leaving a list)
- Route transition animations
Setup
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';
// or provideAnimationsAsync() for lazy-loading the animation engine
export const appConfig: ApplicationConfig = {
providers: [
provideAnimations(),
],
};provideAnimationsAsync() if you want to defer loading the animation engine until it's first needed — good for reducing initial bundle size.Animation Building Blocks
Function | Purpose |
|---|---|
trigger(name, [...]) | Names an animation and defines its states/transitions |
state(name, style({...})) | Defines the styles for a named state |
transition(expr, [...]) | Defines animation between states |
animate(timing, style?) | Performs the animation over time |
style({...}) | Sets CSS styles at a keyframe |
keyframes([...]) | Multi-step animation within a transition |
query(selector, [...]) | Selects child elements for animation |
stagger(delay, [...]) | Staggers animations for a list of elements |
group([...]) | Runs animations in parallel |
sequence([...]) | Runs animations one after another |
Your First Animation — Fade In/Out
// src/app/components/alert/alert.component.ts
import { Component } from '@angular/core';
import {
trigger, state, style, transition, animate
} from '@angular/animations';
@Component({
selector: 'app-alert',
standalone: true,
animations: [
trigger('fadeInOut', [
// Define the two states
state('visible', style({ opacity: 1, transform: 'translateY(0)' })),
state('hidden', style({ opacity: 0, transform: 'translateY(-10px)' })),
// Transitions between states
transition('hidden => visible', animate('300ms ease-in')),
transition('visible => hidden', animate('200ms ease-out')),
]),
],
template: `
<div [@fadeInOut]="isVisible ? 'visible' : 'hidden'" class="alert">
{{ message }}
</div>
<button (click)="toggle()">Toggle</button>
`,
})
export class AlertComponent {
isVisible = true;
message = 'This is an animated alert!';
toggle(): void { this.isVisible = !this.isVisible; }
}void State — Enter and Leave
The special void state represents the element being absent from the DOM (before it enters or after it leaves). This is the key to animating @if and *ngIf.
import { trigger, transition, style, animate } from '@angular/animations';
// Shorthand: ':enter' = 'void => *', ':leave' = '* => void'
export const slideInOut = trigger('slideInOut', [
transition(':enter', [
style({ opacity: 0, height: '0px', overflow: 'hidden' }),
animate('300ms ease-out', style({ opacity: 1, height: '*' })),
]),
transition(':leave', [
animate('200ms ease-in', style({ opacity: 0, height: '0px' })),
]),
]);
// Usage in component
@Component({
animations: [slideInOut],
template: `
@if (showPanel) {
<div @slideInOut class="panel">
<p>This panel slides in and out!</p>
</div>
}
<button (click)="showPanel = !showPanel">Toggle Panel</button>
`,
})
export class PanelComponent {
showPanel = false;
}* value in style() means "the element's natural/auto value". Using height: '*' lets Angular calculate the actual height dynamically.Keyframe Animations
Use keyframes() for multi-step animations within a single transition.
import { trigger, transition, animate, keyframes, style } from '@angular/animations';
export const bounce = trigger('bounce', [
transition(':enter', [
animate('600ms', keyframes([
style({ transform: 'translateY(-60px)', opacity: 0, offset: 0 }),
style({ transform: 'translateY(10px)', opacity: 1, offset: 0.6 }),
style({ transform: 'translateY(-5px)', opacity: 1, offset: 0.8 }),
style({ transform: 'translateY(0px)', opacity: 1, offset: 1.0 }),
])),
]),
]);
// Shake animation for form validation errors
export const shake = trigger('shake', [
transition('* => error', [
animate('500ms', keyframes([
style({ transform: 'translateX(0)', offset: 0 }),
style({ transform: 'translateX(-10px)', offset: 0.25 }),
style({ transform: 'translateX(10px)', offset: 0.50 }),
style({ transform: 'translateX(-10px)', offset: 0.75 }),
style({ transform: 'translateX(0)', offset: 1 }),
])),
]),
]);List Animation — Stagger
Animating list items one after another creates an elegant cascade effect. Use query and stagger inside a parent trigger.
import { trigger, transition, query, stagger, animate, style } from '@angular/animations';
export const listAnimation = trigger('listAnimation', [
transition('* => *', [ // fires whenever the list changes
query(':enter', [
style({ opacity: 0, transform: 'translateX(-20px)' }),
stagger(60, [ // 60ms between each item
animate('300ms ease-out', style({ opacity: 1, transform: 'translateX(0)' }))
]),
], { optional: true }),
query(':leave', [
stagger(40, [
animate('200ms ease-in', style({ opacity: 0, transform: 'translateX(20px)' }))
]),
], { optional: true }),
]),
]);
@Component({
animations: [listAnimation],
template: `
<ul [@listAnimation]="items.length">
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
}
</ul>
<button (click)="addItem()">Add</button>
<button (click)="removeFirst()">Remove First</button>
`,
})
export class ListComponent {
items = [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }];
counter = 3;
addItem(): void {
this.items = [...this.items, { id: this.counter, name: `Item ${this.counter++}` }];
}
removeFirst(): void {
this.items = this.items.slice(1);
}
}Route Transition Animations
// src/app/animations/route.animations.ts
import { trigger, transition, style, animate, query, group } from '@angular/animations';
export const slideRoute = trigger('routeAnimations', [
transition('* <=> *', [
query(':enter, :leave', [
style({ position: 'absolute', top: 0, left: 0, width: '100%' }),
]),
group([
query(':leave', [
animate('300ms ease-out', style({ opacity: 0, transform: 'translateX(-30px)' })),
]),
query(':enter', [
style({ opacity: 0, transform: 'translateX(30px)' }),
animate('300ms ease-out', style({ opacity: 1, transform: 'translateX(0)' })),
]),
]),
]),
]);
// app.component.ts
@Component({
animations: [slideRoute],
template: `
<main [@routeAnimations]="getRouteAnimation(outlet)">
<router-outlet #outlet="outlet" />
</main>
`,
})
export class AppComponent {
getRouteAnimation(outlet: RouterOutlet): string {
return outlet?.activatedRouteData?.['animation'] || '';
}
}
// app.routes.ts
{ path: 'home', component: HomeComponent, data: { animation: 'HomePage' } },
{ path: 'about', component: AboutComponent, data: { animation: 'AboutPage' } },Animation Callbacks
@Component({
animations: [trigger('expand', [
transition(':enter', [
style({ height: 0 }),
animate('300ms', style({ height: '*' })),
]),
])],
template: `
<div
@expand
(@expand.start)="onStart($event)"
(@expand.done)="onDone($event)"
>
Content
</div>
`,
})
export class AnimatedComponent {
onStart(event: AnimationEvent): void {
console.log('Animation started:', event.triggerName, event.toState);
}
onDone(event: AnimationEvent): void {
console.log('Animation done:', event.totalTime, 'ms');
}
}Disabling Animations
// Disable all animations on a host element (useful for testing or reduced-motion)
@Component({
template: `<div [@.disabled]="prefersReducedMotion">...</div>`,
})
export class AccessibleComponent {
prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}prefers-reduced-motion media query. Use [@.disabled]="true" to disable animations when the user has requested reduced motion.Best Practices
Extract reusable animations to separate files (e.g., src/app/animations/)
Use :enter/:leave shorthand instead of void => * / * => void
Use { optional: true } in query() to avoid errors when no elements match
Respect prefers-reduced-motion for accessibility
Keep animations short (150–400ms) — long animations feel sluggish
Use provideAnimationsAsync() for better initial load performance
Test animations with AnimationTestingModule or disable them in unit tests
Use group() for parallel animations, sequence() for sequential ones
Summary
Angular's animation system covers everything from simple fade-ins to complex staggered list animations and route transitions. The declarative API (trigger, state, transition, animate) keeps animation logic co-located with the component and away from CSS files. For production apps, always extract animation definitions into separate files, add animation callbacks for coordination, and disable animations for users who prefer reduced motion.