> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 18. Angular Animations

> Create fluid, performant animations using Angular's animation system

<Frame>
  <img src="https://mintcdn.com/devweeekends/AEOaWh79Ur7CdHHv/images/courses/angular-crash-course/angular-hero.svg?fit=max&auto=format&n=AEOaWh79Ur7CdHHv&q=85&s=32645ae19fa9bc25d3ec281022aba371" alt="Angular Animations" width="1200" height="400" data-path="images/courses/angular-crash-course/angular-hero.svg" />
</Frame>

## Animations Overview

<Info>
  **Estimated Time**: 2 hours | **Difficulty**: Intermediate | **Prerequisites**: Components, Templates
</Info>

Angular's animation system is built on the Web Animations API, providing a powerful DSL (Domain-Specific Language) for creating complex, performant animations. Unlike CSS animations, Angular animations are state-aware -- they know about your component's data, can react to route changes, and can coordinate across multiple elements. Think of CSS animations as a record player (plays the same thing every time) versus Angular animations as a live DJ (responds to what is happening in real-time).

**When to use Angular animations vs CSS**: Use CSS for simple hover effects, loading spinners, or animations that do not depend on component state. Use Angular animations when the animation needs to react to data changes, coordinate enter/leave transitions, stagger list items, or synchronize with route navigation.

```
┌─────────────────────────────────────────────────────────────────────────┐
│              Angular Animation Building Blocks                           │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   trigger('name', [...])                                                │
│       │                                                                  │
│       ├── state('stateName', style({...}))                              │
│       │       Define styles for a specific state                         │
│       │                                                                  │
│       ├── transition('stateA => stateB', [...])                         │
│       │       │                                                          │
│       │       ├── animate('duration easing', style({...}))              │
│       │       │       Animate to a style                                 │
│       │       │                                                          │
│       │       ├── group([...])                                          │
│       │       │       Run animations in parallel                         │
│       │       │                                                          │
│       │       ├── sequence([...])                                       │
│       │       │       Run animations in sequence                         │
│       │       │                                                          │
│       │       ├── query('selector', [...])                              │
│       │       │       Animate child elements                             │
│       │       │                                                          │
│       │       └── stagger('timing', [...])                              │
│       │               Stagger animations for multiple elements           │
│       │                                                                  │
│       └── transition(':enter/:leave', [...])                            │
│               Animate element entry/exit                                 │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
```

***

## Setup

```typescript theme={null}
// app.config.ts
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';

export const appConfig: ApplicationConfig = {
  providers: [
    // provideAnimationsAsync() lazy-loads the animation engine (~60KB),
    // so users who never trigger an animation don't pay the cost.
    // Use provideAnimations() instead only if animations appear on the
    // initial render and you can't afford the flash of unstyled content.
    provideAnimationsAsync()
  ]
};
```

<Warning>
  **Common gotcha**: If you forget to provide either `provideAnimations()` or `provideAnimationsAsync()`, your `[@trigger]` bindings will silently do nothing. No error, no animation -- just a confusing lack of movement. This is the single most common "why isn't my animation working?" issue.
</Warning>

***

## Basic Animations

### State-Based Animation

```typescript theme={null}
// expandable-panel.component.ts
import { Component, signal } from '@angular/core';
import { 
  trigger, 
  state, 
  style, 
  transition, 
  animate 
} from '@angular/animations';

@Component({
  selector: 'app-expandable-panel',
  standalone: true,
  animations: [
    trigger('expandCollapse', [
      state('collapsed', style({
        height: '0px',
        opacity: 0,
        overflow: 'hidden'
      })),
      state('expanded', style({
        height: '*',  // Auto height
        opacity: 1,
        overflow: 'visible'
      })),
      transition('collapsed <=> expanded', [
        animate('300ms ease-in-out')
      ])
    ])
  ],
  template: `
    <div class="panel">
      <div class="header" (click)="toggle()">
        <span>{{ title }}</span>
        <span class="icon" [@rotateIcon]="isExpanded() ? 'expanded' : 'collapsed'">
          ▼
        </span>
      </div>
      <div class="content" [@expandCollapse]="isExpanded() ? 'expanded' : 'collapsed'">
        <ng-content />
      </div>
    </div>
  `
})
export class ExpandablePanelComponent {
  title = input('Panel');
  isExpanded = signal(false);
  
  toggle() {
    this.isExpanded.update(v => !v);
  }
}
```

### Enter/Leave Animations

```typescript theme={null}
// notification.component.ts
@Component({
  selector: 'app-notification',
  standalone: true,
  animations: [
    trigger('slideInOut', [
      transition(':enter', [
        style({ 
          transform: 'translateX(100%)', 
          opacity: 0 
        }),
        animate('300ms ease-out', style({ 
          transform: 'translateX(0)', 
          opacity: 1 
        }))
      ]),
      transition(':leave', [
        animate('200ms ease-in', style({ 
          transform: 'translateX(100%)', 
          opacity: 0 
        }))
      ])
    ])
  ],
  template: `
    @if (visible()) {
      <div class="notification" [@slideInOut] [class]="type()">
        <span class="message">{{ message() }}</span>
        <button (click)="dismiss()">×</button>
      </div>
    }
  `
})
export class NotificationComponent {
  message = input.required<string>();
  type = input<'success' | 'error' | 'warning'>('success');
  visible = signal(true);
  dismissed = output<void>();
  
  dismiss() {
    this.visible.set(false);
    this.dismissed.emit();
  }
}
```

***

## Advanced Animations

### Staggered List Animation

Staggering creates a "cascade" effect where each item animates with a slight delay after the previous one. This is the animation pattern that makes list UIs feel polished -- think of how iOS notification center items slide in one after another rather than all at once. The key is `stagger()`, which adds an incremental delay to each `:enter` or `:leave` element matched by `query()`.

```typescript theme={null}
// task-list.component.ts
@Component({
  selector: 'app-task-list',
  standalone: true,
  animations: [
    trigger('listAnimation', [
      transition('* => *', [
        query(':enter', [
          style({ opacity: 0, transform: 'translateY(-20px)' }),
          stagger('50ms', [
            animate('300ms ease-out', style({ 
              opacity: 1, 
              transform: 'translateY(0)' 
            }))
          ])
        ], { optional: true }),
        query(':leave', [
          stagger('30ms', [
            animate('200ms ease-in', style({ 
              opacity: 0, 
              transform: 'translateX(-100%)' 
            }))
          ])
        ], { optional: true })
      ])
    ]),
    
    trigger('taskItem', [
      state('active', style({
        backgroundColor: '#e0f2fe',
        transform: 'scale(1.02)'
      })),
      state('completed', style({
        backgroundColor: '#dcfce7',
        opacity: 0.7
      })),
      transition('* => active', animate('200ms ease-out')),
      transition('active => completed', [
        animate('300ms ease-in', style({
          backgroundColor: '#dcfce7'
        })),
        animate('200ms', style({
          opacity: 0.7
        }))
      ])
    ])
  ],
  template: `
    <div class="task-list" [@listAnimation]="tasks().length">
      @for (task of tasks(); track task.id) {
        <div 
          class="task-item" 
          [@taskItem]="task.status"
          (click)="toggleTask(task)"
        >
          <input 
            type="checkbox" 
            [checked]="task.status === 'completed'"
            (change)="toggleTask(task)"
          />
          <span [class.completed]="task.status === 'completed'">
            {{ task.title }}
          </span>
        </div>
      }
    </div>
  `
})
export class TaskListComponent {
  tasks = input.required<Task[]>();
  taskToggled = output<Task>();
  
  toggleTask(task: Task) {
    this.taskToggled.emit(task);
  }
}
```

### Page Transition Animation

```typescript theme={null}
// app.component.ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  animations: [
    trigger('routeAnimations', [
      transition('* <=> *', [
        // Query outgoing page
        query(':leave', [
          style({ position: 'absolute', width: '100%' }),
          animate('300ms ease-in', style({
            opacity: 0,
            transform: 'translateX(-100%)'
          }))
        ], { optional: true }),
        
        // Query incoming page
        query(':enter', [
          style({
            position: 'absolute',
            width: '100%',
            opacity: 0,
            transform: 'translateX(100%)'
          }),
          animate('300ms ease-out', style({
            opacity: 1,
            transform: 'translateX(0)'
          }))
        ], { optional: true })
      ])
    ])
  ],
  template: `
    <div class="app-container" [@routeAnimations]="getRouteAnimationData()">
      <router-outlet />
    </div>
  `
})
export class AppComponent {
  private route = inject(ActivatedRoute);
  
  getRouteAnimationData() {
    return this.route.firstChild?.snapshot?.data?.['animation'] ?? 'default';
  }
}

// app.routes.ts
export const routes: Routes = [
  { 
    path: '', 
    loadComponent: () => import('./home.component'),
    data: { animation: 'HomePage' }
  },
  { 
    path: 'about', 
    loadComponent: () => import('./about.component'),
    data: { animation: 'AboutPage' }
  }
];
```

### Complex Multi-Step Animation

```typescript theme={null}
// card-flip.component.ts
@Component({
  selector: 'app-card-flip',
  standalone: true,
  animations: [
    trigger('cardFlip', [
      state('front', style({
        transform: 'rotateY(0deg)'
      })),
      state('back', style({
        transform: 'rotateY(180deg)'
      })),
      transition('front => back', [
        group([
          query('.front', [
            animate('400ms ease-in', style({
              transform: 'rotateY(180deg)'
            }))
          ]),
          query('.back', [
            style({ transform: 'rotateY(-180deg)' }),
            animate('400ms ease-out', style({
              transform: 'rotateY(0deg)'
            }))
          ])
        ])
      ]),
      transition('back => front', [
        group([
          query('.front', [
            style({ transform: 'rotateY(180deg)' }),
            animate('400ms ease-out', style({
              transform: 'rotateY(0deg)'
            }))
          ]),
          query('.back', [
            animate('400ms ease-in', style({
              transform: 'rotateY(-180deg)'
            }))
          ])
        ])
      ])
    ])
  ],
  template: `
    <div 
      class="card-container" 
      [@cardFlip]="side()"
      (click)="flip()"
    >
      <div class="card front">
        <ng-content select="[front]" />
      </div>
      <div class="card back">
        <ng-content select="[back]" />
      </div>
    </div>
  `,
  styles: [`
    .card-container {
      perspective: 1000px;
      cursor: pointer;
    }
    .card {
      backface-visibility: hidden;
      position: absolute;
      width: 100%;
      height: 100%;
    }
    .back {
      transform: rotateY(180deg);
    }
  `]
})
export class CardFlipComponent {
  side = signal<'front' | 'back'>('front');
  
  flip() {
    this.side.update(s => s === 'front' ? 'back' : 'front');
  }
}
```

***

## Reusable Animation Functions

```typescript theme={null}
// animations/shared.animations.ts
import { 
  animation, 
  style, 
  animate, 
  trigger, 
  transition, 
  useAnimation,
  query,
  stagger,
  keyframes
} from '@angular/animations';

// Reusable animation definitions
export const fadeIn = animation([
  style({ opacity: 0 }),
  animate('{{ duration }} {{ easing }}', style({ opacity: 1 }))
], {
  params: { duration: '300ms', easing: 'ease-out' }
});

export const fadeOut = animation([
  animate('{{ duration }} {{ easing }}', style({ opacity: 0 }))
], {
  params: { duration: '200ms', easing: 'ease-in' }
});

export const slideInLeft = animation([
  style({ transform: 'translateX(-100%)', opacity: 0 }),
  animate('{{ duration }} {{ easing }}', style({ 
    transform: 'translateX(0)', 
    opacity: 1 
  }))
], {
  params: { duration: '300ms', easing: 'ease-out' }
});

export const slideOutRight = animation([
  animate('{{ duration }} {{ easing }}', style({ 
    transform: 'translateX(100%)', 
    opacity: 0 
  }))
], {
  params: { duration: '200ms', easing: 'ease-in' }
});

export const bounce = animation([
  animate('{{ duration }}', keyframes([
    style({ transform: 'translateY(0)', offset: 0 }),
    style({ transform: 'translateY(-30px)', offset: 0.3 }),
    style({ transform: 'translateY(0)', offset: 0.5 }),
    style({ transform: 'translateY(-15px)', offset: 0.7 }),
    style({ transform: 'translateY(0)', offset: 1 })
  ]))
], {
  params: { duration: '500ms' }
});

export const shake = animation([
  animate('{{ duration }}', keyframes([
    style({ transform: 'translateX(0)', offset: 0 }),
    style({ transform: 'translateX(-10px)', offset: 0.2 }),
    style({ transform: 'translateX(10px)', offset: 0.4 }),
    style({ transform: 'translateX(-10px)', offset: 0.6 }),
    style({ transform: 'translateX(10px)', offset: 0.8 }),
    style({ transform: 'translateX(0)', offset: 1 })
  ]))
], {
  params: { duration: '400ms' }
});

// Reusable trigger factories
export function fadeInOutTrigger(name: string = 'fadeInOut') {
  return trigger(name, [
    transition(':enter', useAnimation(fadeIn)),
    transition(':leave', useAnimation(fadeOut))
  ]);
}

export function slideInOutTrigger(name: string = 'slideInOut') {
  return trigger(name, [
    transition(':enter', useAnimation(slideInLeft)),
    transition(':leave', useAnimation(slideOutRight))
  ]);
}

export function listStaggerTrigger(name: string = 'listStagger') {
  return trigger(name, [
    transition('* => *', [
      query(':enter', [
        style({ opacity: 0, transform: 'translateY(-15px)' }),
        stagger('50ms', [
          animate('300ms ease-out', style({ 
            opacity: 1, 
            transform: 'translateY(0)' 
          }))
        ])
      ], { optional: true })
    ])
  ]);
}

// Usage in component
@Component({
  animations: [
    fadeInOutTrigger('notification'),
    listStaggerTrigger('items'),
    trigger('attention', [
      transition('* => bounce', useAnimation(bounce)),
      transition('* => shake', useAnimation(shake))
    ])
  ],
  template: `
    @if (showNotification()) {
      <div [@notification]>Notification</div>
    }
    
    <div [@items]="items().length">
      @for (item of items(); track item.id) {
        <div>{{ item.name }}</div>
      }
    </div>
    
    <button 
      [@attention]="attentionState()" 
      (@attention.done)="onAnimationDone()"
    >
      Click me
    </button>
  `
})
export class AnimatedComponent {
  showNotification = signal(true);
  items = signal<Item[]>([]);
  attentionState = signal('');
  
  triggerBounce() {
    this.attentionState.set('bounce');
  }
  
  triggerShake() {
    this.attentionState.set('shake');
  }
  
  onAnimationDone() {
    this.attentionState.set('');
  }
}
```

***

## Animation Callbacks

```typescript theme={null}
@Component({
  animations: [
    trigger('flyInOut', [
      transition(':enter', [
        style({ transform: 'translateX(-100%)' }),
        animate('300ms ease-out')
      ]),
      transition(':leave', [
        animate('300ms ease-in', style({ transform: 'translateX(100%)' }))
      ])
    ])
  ],
  template: `
    @if (visible()) {
      <div 
        [@flyInOut]
        (@flyInOut.start)="onAnimationStart($event)"
        (@flyInOut.done)="onAnimationDone($event)"
      >
        Animated content
      </div>
    }
  `
})
export class CallbackComponent {
  visible = signal(true);
  isAnimating = signal(false);
  
  onAnimationStart(event: AnimationEvent) {
    this.isAnimating.set(true);
    console.log('Animation started:', {
      fromState: event.fromState,
      toState: event.toState,
      totalTime: event.totalTime
    });
  }
  
  onAnimationDone(event: AnimationEvent) {
    this.isAnimating.set(false);
    console.log('Animation completed:', event.toState);
  }
}
```

***

## Performance Tips

<CardGroup cols={2}>
  <Card title="Use transform & opacity" icon="bolt">
    Animate only transform and opacity for 60fps - they're GPU accelerated
  </Card>

  <Card title="will-change Hint" icon="microchip">
    Use `will-change: transform` for complex animations
  </Card>

  <Card title="Disable When Not Visible" icon="eye-slash">
    Use `@.disabled` binding to disable animations conditionally
  </Card>

  <Card title="Lazy Load Animations" icon="clock">
    Use `provideAnimationsAsync()` to lazy-load animation code
  </Card>
</CardGroup>

```typescript theme={null}
// Disable animations conditionally
@Component({
  template: `
    <div 
      [@myAnimation]="state"
      [@.disabled]="prefersReducedMotion()"
    >
      Content
    </div>
  `
})
export class AccessibleComponent {
  private mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
  prefersReducedMotion = signal(this.mediaQuery.matches);
  
  constructor() {
    this.mediaQuery.addEventListener('change', (e) => {
      this.prefersReducedMotion.set(e.matches);
    });
  }
}
```

***

<Card title="Next: Internationalization (i18n)" icon="arrow-right" href="/courses/angular-crash-course/19-i18n">
  Add multi-language support to your Angular applications
</Card>
