AngularJSAnimations

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 / @if toggles)
  • State-based animations (expanded/collapsed, active/inactive)
  • List animations (items entering and leaving a list)
  • Route transition animations
Setup

TS
// 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(),
  ],
};
Note
Use 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

TS
// 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.

TS
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;
}
Tip
The * 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.

TS
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.

TS
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

TS
// 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

TS
@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

TS
// 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;
}
Warning
Always respect the user's 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.