Say Hello to Native CSS Animations and Goodbye to @angular/animations
Amos Isaila

Amos Isaila @amosisaila

About: Angular enthusiast. https://www.youtube.com/channel/UCATsbPmCzU5dVL_sbNwbb5w

Location:
Spain
Joined:
May 8, 2017

Say Hello to Native CSS Animations and Goodbye to @angular/animations

Publish Date: Aug 6
0 0

The framework is deprecating the @angular/animations package in favor of a new approach: native CSS animations integration through animate.enter and animate.leave.

Angular 20.2.0: Native CSS Animations

Why @angular/animations Is Being Retired

After eight years of service, @angular/animations is showing its age. Created before modern CSS features like @keyframes and hardware-accelerated transforms were widely supported, the package solved important problems, but now creates new ones:

Size Impact : the package adds approximately 60KB to your bundle size

Performance Issues : animations run in JavaScript without hardware acceleration

Learning Curve : Angular-specific API that doesn’t translate to other frameworks

Integration Friction : difficlt to use with popular third-party animation libraries like GSAP or Anime.js

Meanwhile, the web platform has evolved dramatically, offering native animation capabilities that are faster, smaller, and more widely applicable.

Meet Your New Animation System: animate.enter and animate.leave

Starting in Angular 20.2.0, two powerful new features provide everything you need for smooth, performant animations:

Basic Usage: CSS Class Application

The simplest approach applies CSS animation classes automatically:

@Component({
  template: `
    @if (showMessage()) {
      <div animate.enter="slide-in" animate.leave="fade-out">
        Welcome to the future of Angular animations!
      </div>
    }
    <button (click)="toggle()">Toggle Message</button>
  `,
  styles: [`
    .slide-in {
      animation: slideIn 0.3s ease-out;
    }

    .fade-out {
      animation: fadeOut 0.2s ease-in;
    }

    @keyframes slideIn {
      from { transform: translateY(-20px); opacity: 0; }
      to { transform: translateY(0); opacity: 1; }
    }

    @keyframes fadeOut {
      from { opacity: 1; }
      to { opacity: 0; }
    }
  `]
})
export class MessageComponent {
  showMessage = signal(false);

  toggle(): void {
    this.showMessage.update((v: boolean) => !v);
  }
}
Enter fullscreen mode Exit fullscreen mode

Advanced Control with Animation Functions

For complex animations or third-party library integration, use function-based control:

@Component({
  template: `
    <div class="animation-playground">
      @if (showElement()) {
        <div class="animated-box"
             (animate.enter)="handleEnterAnimation($event)"
             (animate.leave)="handleLeaveAnimation($event)">
          <h3>Advanced Animation</h3>
          <p>Powered by GSAP integration</p>
        </div>
      }

      <button (click)="toggleElement()">
        {{showElement() ? 'Hide' : 'Show'}} Element
      </button>
    </div>
  `,
  styles: [`
    .animated-box {
      width: 300px;
      height: 200px;
      background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
      border-radius: 12px;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      color: white;
      margin: 20px 0;
    }
  `]
})
export class AdvancedAnimationComponent {
  showElement = signal(false);

  handleEnterAnimation(event: AnimationCallbackEvent): void {
    // Using GSAP for complex enter animation
    gsap.fromTo(event.target, 
      {
        scale: 0,
        rotation: -180,
        opacity: 0
      },
      {
        scale: 1,
        rotation: 0,
        opacity: 1,
        duration: 0.8,
        ease: "back.out(1.7)",
        onComplete: () => {
          // Animation complete callback is automatic for enter animations
          console.log('Enter animation completed!');
        }
      }
    );
  }

  handleLeaveAnimation(event: AnimationCallbackEvent): void {
    // Complex leave animation with staggered effects
    const timeline = gsap.timeline({
      onComplete: () => {
        // Must call this to complete the removal process
        event.animationComplete();
      }
    });

    timeline
      .to(event.target, {
        scale: 1.1,
        duration: 0.1,
        ease: "power2.out"
      })
      .to(event.target, {
        scale: 0,
        rotation: 180,
        opacity: 0,
        duration: 0.5,
        ease: "power2.in"
      });
  }

  toggleElement(): void {
    this.showElement.update((v: boolean) => !v);
  }
}
Enter fullscreen mode Exit fullscreen mode

Host Element Animations

Apply animations directly to component host elements:

@Component({
  selector: 'notification-card',
  template: `
    <div class="notification-content">
      <h4>{{title()}}</h4>
      <p>{{message()}}</p>
      <button (click)="dismiss()">×</button>
    </div>
  `,
  host: {
    '[animate.enter]': 'enterAnimation',
    '[animate.leave]': 'leaveAnimation',
    'class': 'notification-card'
  },
  styles: [`
    :host {
      display: block;
      background: white;
      border-radius: 8px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
      padding: 16px;
      margin: 8px 0;
      max-width: 400px;
    }

    .slide-in-right {
      animation: slideInRight 0.3s ease-out;
    }

    .slide-out-right {
      animation: slideOutRight 0.3s ease-in;
    }

    @keyframes slideInRight {
      from { transform: translateX(100%); opacity: 0; }
      to { transform: translateX(0); opacity: 1; }
    }

    @keyframes slideOutRight {
      from { transform: translateX(0); opacity: 1; }
      to { transform: translateX(100%); opacity: 0; }
    }
  `]
})
export class NotificationCardComponent {
  title = input.required<string>();
  message = input.required<string>();
  type = input<'success' | 'warning' | 'error'>('success');
  autoClose = input<boolean>(true);

  dismissed = output<void>();

  enterAnimation = 'slide-in-right';
  leaveAnimation = 'slide-out-right';

  // Auto-close functionality using signals
  private autoCloseTimer = signal<ReturnType<typeof setTimeout> | null>(null);

  constructor() {
    // Set up auto-close when enabled
    effect(() => {
      if (this.autoClose()) {
        const timer = setTimeout(() => this.dismiss(), 5000);
        this.autoCloseTimer.set(timer);
      }
    });
  }

  dismiss() {
    const timer = this.autoCloseTimer();
    if (timer) {
      clearTimeout(timer);
      this.autoCloseTimer.set(null);
    }
    this.dismissed.emit();
  }
}

// Usage in parent component with signal inputs
@Component({
  template: `
    <div class="notifications">
      @for (notification of notifications(); track notification.id) {
        <notification-card 
          [title]="notification.title"
          [message]="notification.message"
          [type]="notification.type"
          [autoClose]="notification.autoClose"
          (dismissed)="removeNotification(notification.id)" />
      }
    </div>

    <div class="controls">
      <button (click)="addSuccessNotification()">Add Success</button>
      <button (click)="addWarningNotification()">Add Warning</button>
      <button (click)="addErrorNotification()">Add Error</button>
    </div>
  `,
  styles: [`
    .notifications {
      position: fixed;
      top: 20px;
      right: 20px;
      z-index: 1000;
    }

    .controls {
      margin: 20px;
    }

    .controls button {
      margin-right: 10px;
      padding: 8px 16px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
  `]
})
export class NotificationContainerComponent {
  notifications = signal<Array<{
    id: number;
    title: string;
    message: string;
    type: 'success' | 'warning' | 'error';
    autoClose: boolean;
  }>>([]);

  private nextId = signal(1);

  addSuccessNotification() {
    this.addNotification({
      title: 'Success!',
      message: 'Operation completed successfully!',
      type: 'success',
      autoClose: true
    });
  }

  addWarningNotification() {
    this.addNotification({
      title: 'Warning',
      message: 'Please review your settings.',
      type: 'warning',
      autoClose: false
    });
  }

  addErrorNotification() {
    this.addNotification({
      title: 'Error',
      message: 'Something went wrong. Please try again.',
      type: 'error',
      autoClose: false
    });
  }

  private addNotification(notificationData: Omit<any, 'id'>) {
    const newNotification = {
      id: this.nextId(),
      ...notificationData
    };

    this.nextId.update(id => id + 1);
    this.notifications.update(notifications => 
      [...notifications, newNotification]
    );
  }

  removeNotification(id: number) {
    this.notifications.update(notifications => 
      notifications.filter(n => n.id !== id)
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Testing Support

Angular provides an ANIMATIONS_DISABLED token for test environments:

TestBed.configureTestingModule({
  providers: [
    { provide: ANIMATIONS_DISABLED, useValue: true }
  ]
});
Enter fullscreen mode Exit fullscreen mode

When disabled, animations complete immediately while still triggering all lifecycle events.

Migration Strategy

For existing @angular/animations users, Angular provides a comprehensive migration guide. The most common patterns translate directly:

Before (Angular Animations):

@Component({
  animations: [
    trigger('slideIn', [
      transition(':enter', [
        style({ transform: 'translateX(-100%)' }),
        animate('300ms ease-in', style({ transform: 'translateX(0%)' }))
      ])
    ])
  ]
})
Enter fullscreen mode Exit fullscreen mode

After (Native Animations):

@Component({
  template: `<div animate.enter="slide-in">`,
  styles: [`
    .slide-in {
      animation: slideIn 300ms ease-in;
    }
    @keyframes slideIn {
      from { transform: translateX(-100%); }
      to { transform: translateX(0%); }
    }
  `]
})
Enter fullscreen mode Exit fullscreen mode

The Future is Bright

This change represents more than just a new API — it’s Angular’s commitment to embracing web standards while providing the developer experience you expect. By moving to native CSS animations, Angular applications become:

  • Faster : hardware-accelerated animations
  • Smaller : no large animation runtime
  • More Portable : skills transfer across frameworks
  • More Flexible : easy third-party library integration

The animations field in component decorators is officially deprecated as of Angular 20.2 and will be removed in version 23, giving developers a clear migration timeline.

Benefits of Angular Native CSS Animations

Thanks for reading so far 🙏

I’d like to have your feedback, so please leave a comment , clap or follow. 👏

Spread the Angular love! 💜

If you liked it, share it among your community, tech bros and whoever you want! 🚀👥

Don’t forget to follow me and stay updated: 📱

Thanks for being part of this Angular journey! 👋😁

Originally published at https://www.codigotipado.com.

Comments 0 total

    Add comment