Say Hello to Native CSS Animations and Goodbye to @angular/animations
The framework is deprecating the @angular/animations package in favor of a new approach: native CSS animations integration through animate.enter and animate.leave.
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);
}
}
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);
}
}
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)
);
}
}
Testing Support
Angular provides an ANIMATIONS_DISABLED
token for test environments:
TestBed.configureTestingModule({
providers: [
{ provide: ANIMATIONS_DISABLED, useValue: true }
]
});
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%)' }))
])
])
]
})
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%); }
}
`]
})
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.
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! 👋😁