Skip to main content

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 using group().
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 using sequence().
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.

Additional Resources