Extended Ecosystem
Animations

Migrating away from Angular's Animations package

Almost all the features supported by @angular/animations have simpler alternatives with native CSS. Consider removing the Angular Animations package from your application, as the package can contribute around 60 kilobytes to your JavaScript bundle. Native CSS animations offer superior performance, as they can benefit from hardware acceleration. Animations defined in the animations package lack that ability. This guide walks through the process of refactoring your code from @angular/animations to native CSS animations.

How to write animations in native CSS

If you've never written any native CSS animations, there are a number of excellent guides to get you started. Here's a few of them:
MDN's CSS Animations guide
W3Schools CSS3 Animations guide
The Complete CSS Animations Tutorial
CSS Animation for Beginners

and a couple of videos:
Learn CSS Animation in 9 Minutes
Net Ninja CSS Animation Tutorial Playlist

Check some of these various guides and tutorials out, and then come back to this guide.

Creating Reusable Animations

Just like with the animations package, you can create reusable animations that can be shared across your application. The animations package version of this had you using the animation() function in a shared typescript file. The native CSS version of this is similar, but lives in a shared CSS file.

With Animations Package

src/app/animations.ts

      
import {animation, style, animate, trigger, transition, useAnimation} from '@angular/animations';export const transitionAnimation = animation([  style({    height: '{{ height }}',    opacity: '{{ opacity }}',    backgroundColor: '{{ backgroundColor }}',  }),  animate('{{ time }}'),]);export const sharedAnimation = animation([  style({    height: 0,    opacity: 1,    backgroundColor: 'red',  }),  animate('1s'),]);export const triggerAnimation = trigger('openClose', [  transition('open => closed', [    useAnimation(transitionAnimation, {      params: {        height: 0,        opacity: 1,        backgroundColor: 'red',        time: '1s',      },    }),  ]),]);

With Native CSS

src/app/animations.css

      
@keyframes sharedAnimation {  to {    height: 0;    opacity: 1;    background-color: 'red';  }}.animated-class {  animation: sharedAnimation 1s;}.open {  height: '200px';  opacity: 1;  background-color: 'yellow';  transition: all 1s;}.closed {  height: '100px';  opacity: 0.8;  background-color: 'blue';  transition: all 1s;}.example-element {  animation-duration: 1s;  animation-delay: 500ms;  animation-timing-function: ease-in-out;}.example-shorthand {  animation: exampleAnimation 1s ease-in-out 500ms;}.example-element {  transition-duration: 1s;  transition-delay: 500ms;  transition-timing-function: ease-in-out;  transition-property: margin-right;}.example-shorthand {  transition: margin-right 1s ease-in-out 500ms;}

Adding the class animated-class to an element would trigger the animation on that element.

Animating a Transition

Animating State and Styles

The animations package allowed you to define various states using the state() function within a component. Examples might be an open or closed state containing the styles for each respective state within the definition. For example:

With Animations Package

src/app/open-close.component.ts

      
import {Component, Input} from '@angular/core';import {trigger, transition, state, animate, style, AnimationEvent} from '@angular/animations';@Component({  selector: 'app-open-close',  animations: [    trigger('openClose', [      // ...      state(        'open',        style({          height: '200px',          opacity: 1,          backgroundColor: 'yellow',        }),      ),      state(        'closed',        style({          height: '100px',          opacity: 0.8,          backgroundColor: 'blue',        }),      ),      transition('open => closed', [animate('1s')]),      transition('closed => open', [animate('0.5s')]),      transition('* => closed', [animate('1s')]),      transition('* => open', [animate('0.5s')]),      transition('open <=> closed', [animate('0.5s')]),      transition('* => open', [animate('1s', style({opacity: '*'}))]),      transition('* => *', [animate('1s')]),    ]),  ],  templateUrl: 'open-close.component.html',  styleUrls: ['open-close.component.css'],})export class OpenCloseComponent {  @Input() logging = false;  isOpen = true;  toggle() {    this.isOpen = !this.isOpen;  }  onAnimationEvent(event: AnimationEvent) {    if (!this.logging) {      return;    }    // openClose is trigger name in this example    console.warn(`Animation Trigger: ${event.triggerName}`);    // phaseName is "start" or "done"    console.warn(`Phase: ${event.phaseName}`);    // in our example, totalTime is 1000 (number of milliseconds in a second)    console.warn(`Total time: ${event.totalTime}`);    // in our example, fromState is either "open" or "closed"    console.warn(`From: ${event.fromState}`);    // in our example, toState either "open" or "closed"    console.warn(`To: ${event.toState}`);    // the HTML element itself, the button in this case    console.warn(`Element: ${event.element}`);  }}

This same behavior can be accomplished natively by using CSS classes either using a keyframe animation or transition styling.

With Native CSS

src/app/animations.css

      
@keyframes sharedAnimation {  to {    height: 0;    opacity: 1;    background-color: 'red';  }}.animated-class {  animation: sharedAnimation 1s;}.open {  height: '200px';  opacity: 1;  background-color: 'yellow';  transition: all 1s;}.closed {  height: '100px';  opacity: 0.8;  background-color: 'blue';  transition: all 1s;}.example-element {  animation-duration: 1s;  animation-delay: 500ms;  animation-timing-function: ease-in-out;}.example-shorthand {  animation: exampleAnimation 1s ease-in-out 500ms;}.example-element {  transition-duration: 1s;  transition-delay: 500ms;  transition-timing-function: ease-in-out;  transition-property: margin-right;}.example-shorthand {  transition: margin-right 1s ease-in-out 500ms;}

Triggering the open or closed state is done by toggling classes on the element in your component. You can find examples of how to do this in our template guide.

You can see similar examples in the template guide for animating styles directly.

Transitions, Timing, and Easing

The animations package animate() function allows for providing timing, like duration, delays and easing. This can be done natively with CSS using several css properties or shorthand properties.

Specify animation-duration, animation-delay, and animation-timing-function for a keyframe animation in CSS, or alternatively use the animation shorthand property.

src/app/animations.css

      
@keyframes sharedAnimation {  to {    height: 0;    opacity: 1;    background-color: 'red';  }}.animated-class {  animation: sharedAnimation 1s;}.open {  height: '200px';  opacity: 1;  background-color: 'yellow';  transition: all 1s;}.closed {  height: '100px';  opacity: 0.8;  background-color: 'blue';  transition: all 1s;}.example-element {  animation-duration: 1s;  animation-delay: 500ms;  animation-timing-function: ease-in-out;}.example-shorthand {  animation: exampleAnimation 1s ease-in-out 500ms;}.example-element {  transition-duration: 1s;  transition-delay: 500ms;  transition-timing-function: ease-in-out;  transition-property: margin-right;}.example-shorthand {  transition: margin-right 1s ease-in-out 500ms;}

Similarly, you can use transition-duration, transition-delay, and transition-timing-function and the transition shorthand for animations that are not using @keyframes.

src/app/animations.css

      
@keyframes sharedAnimation {  to {    height: 0;    opacity: 1;    background-color: 'red';  }}.animated-class {  animation: sharedAnimation 1s;}.open {  height: '200px';  opacity: 1;  background-color: 'yellow';  transition: all 1s;}.closed {  height: '100px';  opacity: 0.8;  background-color: 'blue';  transition: all 1s;}.example-element {  animation-duration: 1s;  animation-delay: 500ms;  animation-timing-function: ease-in-out;}.example-shorthand {  animation: exampleAnimation 1s ease-in-out 500ms;}.example-element {  transition-duration: 1s;  transition-delay: 500ms;  transition-timing-function: ease-in-out;  transition-property: margin-right;}.example-shorthand {  transition: margin-right 1s ease-in-out 500ms;}

Triggering an Animation

The animations package required specifying triggers using the trigger() function and nesting all of your states within it. With native CSS, this is unnecessary. Animations can be triggered by toggling CSS styles or classes. Once a class is present on an element, the animation will occur. Removing the class will revert the element back to whatever CSS is defined for that element. This results in significantly less code to do the same animation. Here's an example:

With Animations Package

src/app/open-close.component.ts

      
import {Component, signal} from '@angular/core';import {trigger, transition, state, animate, style, keyframes} from '@angular/animations';@Component({  selector: 'app-open-close',  animations: [    trigger('openClose', [      state(        'open',        style({          height: '200px',          opacity: 1,          backgroundColor: 'yellow',        }),      ),      state(        'closed',        style({          height: '100px',          opacity: 0.5,          backgroundColor: 'green',        }),      ),      // ...      transition('* => *', [        animate(          '1s',          keyframes([            style({opacity: 0.1, offset: 0.1}),            style({opacity: 0.6, offset: 0.2}),            style({opacity: 1, offset: 0.5}),            style({opacity: 0.2, offset: 0.7}),          ]),        ),      ]),    ]),  ],  templateUrl: 'open-close.component.html',  styleUrl: 'open-close.component.css',})export class OpenCloseComponent {  isOpen = signal(false);  toggle() {    this.isOpen.update((isOpen) => !isOpen);  }}

src/app/open-close.component.html

      
<nav>  <button type="button" (click)="toggle()">Toggle Open/Close</button></nav><div [@openClose]="isOpen() ? 'open' : 'closed'" class="open-close-container">  <p>The box is now {{ isOpen() ? 'Open' : 'Closed' }}!</p></div>

src/app/open-close.component.css

      
:host {  display: block;  margin-top: 1rem;}.open-close-container {  border: 1px solid #dddddd;  margin-top: 1em;  padding: 20px 20px 0px 20px;  color: #000000;  font-weight: bold;  font-size: 20px;}

With Native CSS

src/app/open-close.component.ts

      
import {Component, signal} from '@angular/core';@Component({  selector: 'app-open-close',  templateUrl: 'open-close.component.html',  styleUrls: ['open-close.component.css'],})export class OpenCloseComponent {  isOpen = signal(true);  toggle() {    this.isOpen.update((isOpen) => !isOpen);  }}

src/app/open-close.component.html

      
<h2>Open / Close Example</h2><button type="button" (click)="toggle()">Toggle Open/Close</button><div class="open-close-container" [class.open]="isOpen()">  <p>The box is now {{ isOpen() ? 'Open' : 'Closed' }}!</p></div>

src/app/open-close.component.css

      
:host {  display: block;  margin-top: 1rem;}.open-close-container {  border: 1px solid #dddddd;  margin-top: 1em;  padding: 20px 20px 0px 20px;  font-weight: bold;  font-size: 20px;  height: 100px;  opacity: 0.8;  background-color: blue;  color: #ebebeb;  transition-property: height, opacity, background-color, color;  transition-duration: 1s;}.open {  transition-duration: 0.5s;  height: 200px;  opacity: 1;  background-color: yellow;  color: #000000;}

Transition and Triggers

Predefined State and wildcard matching

The animations package offers the ability to match your defined states to a transition via strings. For example, animating from open to closed would be open => closed. You can use wildcards to match any state to a target state, like * => closed and the void keyword can be used for entering and exiting states. For example: * => void for when an element leaves a view or void => * for when the element enters a view.

These state matching patterns are not needed at all when animating with CSS directly. You can manage what transitions and @keyframes animations apply based on whatever classes you set and / or styles you set on the elements. You can also add @starting-style to control how the element looks upon immediately entering the DOM.

Automatic Property Calculation with Wildcards

The animations package offers the ability to animate things that have been historically difficult to animate, like animating a set height to height: auto. You can now do this with pure CSS as well.

With Animations Package

src/app/auto-height.component.ts

      
import {Component, signal} from '@angular/core';import {trigger, transition, state, animate, style} from '@angular/animations';@Component({  selector: 'app-open-close',  animations: [    trigger('openClose', [      state('true', style({height: '*'})),      state('false', style({height: '0px'})),      transition('false <=> true', animate(1000)),    ]),  ],  templateUrl: 'auto-height.component.html',  styleUrl: 'auto-height.component.css',})export class AutoHeightComponent {  isOpen = signal(false);  toggle() {    this.isOpen.update((isOpen) => !isOpen);  }}

src/app/auto-height.component.html

      
<h2>Auto Height Example</h2><button type="button" (click)="toggle()">Toggle Open/Close</button><div class="container" [@openClose]="isOpen() ? true : false">  <div class="content">    <p>The box is now {{ isOpen() ? 'Open' : 'Closed' }}!</p>  </div></div>

src/app/auto-height.component.css

      
.container {  display: block;  overflow: hidden;}.container .content {  padding: 20px;  margin-top: 1em;  font-weight: bold;  font-size: 20px;  background-color: blue;  color: #ebebeb;}

You can use css-grid to animate to auto height.

With Native CSS

src/app/auto-height.component.ts

      
import {Component, signal} from '@angular/core';@Component({  selector: 'app-auto-height',  templateUrl: 'auto-height.component.html',  styleUrls: ['auto-height.component.css'],})export class AutoHeightComponent {  isOpen = signal(true);  toggle() {    this.isOpen.update((isOpen) => !isOpen);  }}

src/app/auto-height.component.html

      
<h2>Auto Height Example</h2><button type="button" (click)="toggle()">Toggle Open/Close</button><div class="container" [class.open]="isOpen()">  <div class="content">    <p>The box is now {{ isOpen() ? 'Open' : 'Closed' }}!</p>  </div></div>

src/app/auto-height.component.css

      
.container {  display: grid;  grid-template-rows: 0fr;  overflow: hidden;  transition: grid-template-rows 1s;}.container.open {  grid-template-rows: 1fr;}.container .content {  min-height: 0;  transition: visibility 1s;  padding: 0 20px;  visibility: hidden;  margin-top: 1em;  font-weight: bold;  font-size: 20px;  background-color: blue;  color: #ebebeb;  overflow: hidden;}.container.open .content {  visibility: visible;}

If you don't have to worry about supporting all browsers, you can also check out calc-size(), which is the true solution to animating auto height. See MDN's docs and (this tutorial)[https://frontendmasters.com/blog/one-of-the-boss-battles-of-css-is-almost-won-transitioning-to-auto/] for more information.

Animate entering and leaving a view

The animations package offered the previously mentioned pattern matching for entering and leaving but also included the shorthand aliases of :enter and :leave.

With Animations Package

src/app/insert-remove.component.ts

      
import {Component} from '@angular/core';import {trigger, transition, animate, style} from '@angular/animations';@Component({  selector: 'app-insert-remove',  animations: [    trigger('myInsertRemoveTrigger', [      transition(':enter', [style({opacity: 0}), animate('200ms', style({opacity: 1}))]),      transition(':leave', [animate('200ms', style({opacity: 0}))]),    ]),  ],  templateUrl: 'insert-remove.component.html',  styleUrls: ['insert-remove.component.css'],})export class InsertRemoveComponent {  isShown = false;  toggle() {    this.isShown = !this.isShown;  }}

src/app/insert-remove.component.html

      
<h2>Insert/Remove</h2><nav>  <button type="button" (click)="toggle()">Toggle Insert/Remove</button></nav>@if (isShown) {  <div @myInsertRemoveTrigger class="insert-remove-container">    <p>The box is inserted</p>  </div>}

src/app/insert-remove.component.css

      
:host {  display: block;}.insert-remove-container {  border: 1px solid #dddddd;  margin-top: 1em;  padding: 20px 20px 0px 20px;  color: #000000;  font-weight: bold;  font-size: 20px;}

Here's how the same thing can be accomplished without the animations package.

With Native CSS

src/app/insert.component.ts

      
import {Component, signal} from '@angular/core';@Component({  selector: 'app-insert',  templateUrl: 'insert.component.html',  styleUrls: ['insert.component.css'],})export class InsertComponent {  isShown = signal(false);  toggle() {    this.isShown.update((isShown) => !isShown);  }}

src/app/insert.component.html

      
<h2>Insert Element Example</h2><nav>  <button type="button" (click)="toggle()">Toggle Element</button></nav>@if (isShown()) {  <div class="insert-container">    <p>The box is inserted</p>  </div>}

src/app/insert.component.css

      
:host {  display: block;}.insert-container {  border: 1px solid #dddddd;  margin-top: 1em;  padding: 20px 20px 0px 20px;  color: #000000;  font-weight: bold;  font-size: 20px;  opacity: 1;  transition: opacity 1s ease-out, transform 1s ease-out;  @starting-style {    opacity: 0;    transform: translateY(20px);  }}

Leaving a view is slightly more complex. The element removal needs to be delayed until the exit animation is complete. This requires a bit of extra code in your component class to accomplish.

With Native CSS

src/app/remove.component.ts

      
import {Component, ElementRef, inject, signal} from '@angular/core';@Component({  selector: 'app-remove',  templateUrl: 'remove.component.html',  styleUrls: ['remove.component.css'],})export class RemoveComponent {  isShown = signal(false);  deleting = signal(false);  private el = inject(ElementRef);  toggle() {    if (this.isShown()) {      const target = this.el.nativeElement.querySelector('.insert-container');      target.addEventListener('transitionend', () => this.hide());      this.deleting.set(true);    } else {      this.isShown.update((isShown) => !isShown);    }  }  hide() {    this.isShown.set(false);    this.deleting.set(false);  }}

src/app/remove.component.html

      
<h2>Remove Element Example</h2><nav>  <button type="button" (click)="toggle()">Toggle Element</button></nav>@if (isShown()) {  <div class="insert-container" [class.deleting]="deleting()">    <p>The box is inserted</p>  </div>}

src/app/remove.component.css

      
:host {  display: block;}.insert-container {  border: 1px solid #dddddd;  margin-top: 1em;  padding: 20px 20px 0px 20px;  color: #000000;  font-weight: bold;  font-size: 20px;  opacity: 1;  transition: opacity 200ms ease-in;  @starting-style {    opacity: 0;  }}.deleting {  opacity: 0;  transform: translateY(20px);  transition: opacity 500ms ease-out, transform 500ms ease-out;}

Animating increment and decrement

Along with the aforementioned :enter and :leave, there's also :increment and :decrement. You can animate these also by adding and removing classes. Unlike the animation package built-in aliases, there is no automatic application of classes when the values go up or down. You can apply the appropriate classes programmatically. Here's an example:

With Animations Package

src/app/increment-decrement.component.ts

      
import {Component, signal} from '@angular/core';import {trigger, transition, animate, style, query, stagger} from '@angular/animations';@Component({  selector: 'app-increment-decrement',  templateUrl: 'increment-decrement.component.html',  styleUrls: ['increment-decrement.component.css'],  animations: [    trigger('incrementAnimation', [      transition(':increment', [        animate('300ms ease-out', style({color: 'green', transform: 'scale(1.3, 1.2)'})),      ]),      transition(':decrement', [        animate('300ms ease-out', style({color: 'red', transform: 'scale(0.8, 0.9)'})),      ]),    ]),  ],})export class IncrementDecrementComponent {  num = signal(0);  modify(n: number) {    this.num.update((v) => (v += n));  }}

src/app/increment-decrement.component.html

      
<h3>Increment and Decrement Example</h3><section>  <p [@incrementAnimation]="num()">Number {{ num() }}</p>  <div class="controls">    <button type="button" (click)="modify(1)">+</button>    <button type="button" (click)="modify(-1)">-</button>  </div></section>

src/app/increment-decrement.component.css

      
:host {  display: block;  font-size: 32px;  margin: 20px;  text-align: center;}section {  border: 1px solid lightgray;  border-radius: 50px;}p {  display: inline-block;  margin: 2rem 0;  text-transform: uppercase;}.controls {  padding-bottom: 2rem;}button {  font: inherit;  border: 0;  background: lightgray;  width: 50px;  border-radius: 10px;}button + button {  margin-left: 10px;}

With Native CSS

src/app/increment-decrement.component.ts

      
import {Component, ElementRef, OnInit, signal, viewChild} from '@angular/core';@Component({  selector: 'app-increment-decrement',  templateUrl: 'increment-decrement.component.html',  styleUrls: ['increment-decrement.component.css'],})export class IncrementDecrementComponent implements OnInit {  num = signal(0);  el = viewChild<ElementRef<HTMLParagraphElement>>('el');  ngOnInit() {    this.el()?.nativeElement.addEventListener('animationend', (ev) => {      if (ev.animationName.endsWith('decrement') || ev.animationName.endsWith('increment')) {        this.animationFinished();      }    });  }  modify(n: number) {    const targetClass = n > 0 ? 'increment' : 'decrement';    this.num.update((v) => (v += n));    this.el()?.nativeElement.classList.add(targetClass);  }  animationFinished() {    this.el()?.nativeElement.classList.remove('increment', 'decrement');  }  ngOnDestroy() {    this.el()?.nativeElement.removeEventListener('animationend', this.animationFinished);  }}

src/app/increment-decrement.component.html

      
<h3>Increment and Decrement Example</h3><section>  <p #el>Number {{ num() }}</p>  <div class="controls">    <button type="button" (click)="modify(1)">+</button>    <button type="button" (click)="modify(-1)">-</button>  </div></section>

src/app/increment-decrement.component.css

      
:host {  display: block;  font-size: 32px;  margin: 20px;  text-align: center;}section {  border: 1px solid lightgray;  border-radius: 50px;}p {  display: inline-block;  margin: 2rem 0;  text-transform: uppercase;}.increment {  animation: increment 300ms;}.decrement {  animation: decrement 300ms;}.controls {  padding-bottom: 2rem;}button {  font: inherit;  border: 0;  background: lightgray;  width: 50px;  border-radius: 10px;}button + button {  margin-left: 10px;}@keyframes increment {  33% {    color: green;    transform: scale(1.3, 1.2);  }  66% {    color: green;    transform: scale(1.2, 1.2);  }  100% {    transform: scale(1, 1);  }}@keyframes decrement {  33% {    color: red;    transform: scale(0.8, 0.9);  }  66% {    color: red;    transform: scale(0.9, 0.9);  }  100% {    transform: scale(1, 1);  }}

Parent / Child Animations

Unlike the animations package, when multiple animations are specified within a given component, no animation has priority over another and nothing blocks any animation from firing. Any sequencing of animations would have to be handled by your definition of your CSS animation, using animation / transition delay, and / or using animationend or transitionend to handle adding the next css to be animated.

Disabling an animation or all animations

With native CSS animations, if you'd like to disable the animations that you've specified, you have multiple options.

  1. Create a custom class that forces animation and transition to none.
      
.no-animation {  animation: none !important;  transition: none !important;}

Applying this class to an element prevents any animation from firing on that element. You could alternatively scope this to your entire DOM or section of your DOM to enforce this behavior. However, this prevents animation events from firing. If you are awaiting animation events for element removal, this solution won't work. A workaround is to set durations to 1 millisecond instead.

  1. Use the prefers-reduced-motion media query to ensure no animations play for users that prefer less animation.

  2. Prevent adding animation classes programatically

Animation Callbacks

The animations package exposed callbacks for you to use in the case that you want to do something when the animation has finished. Native CSS animations also have these callbacks.

OnAnimationStart
OnAnimationEnd
OnAnimationIteration
OnAnimationCancel

OnTransitionStart
OnTransitionRun
OnTransitionEnd
OnTransitionCancel

The Web Animations API has a lot of additional functionality. Take a look at the documentation to see all the available animation APIs.

NOTE: Be aware of bubbling issues with these callbacks. If you are animating children and parents, the events bubble up from children to parents. Consider stopping propagation or looking at more details within the event to determine if you're responding to the desired event target rather than an event bubbling up from a child node. You can examine the animationname property or the properties being transitioned to verify you have the right nodes.

Complex Sequences

The animations package has built-in functionality for creating complex sequences. These sequences are all entirely possible without the animations package.

Targeting specific elements

In the animations package, you could target specific elements by using the query() function to find specific elements by a CSS class name, similar to document.querySelector(). This is unnecessary in a native CSS animation world. Instead, you can use your CSS selectors to target sub-classes and apply any desired transform or animation.

To toggle classes for child nodes within a template, you can use class and style bindings to add the animations at the right points.

Stagger()

The stagger() function allowed you to delay the animation of each item in a list of items by a specified time to create a cascade effect. You can replicate this behavior in native CSS by utilizing animation-delay or transition-delay. Here is an example of what that CSS might look like.

With Animations Package

src/app/stagger.component.ts

      
import {Component, HostBinding, signal} from '@angular/core';import {trigger, transition, animate, style, query, stagger} from '@angular/animations';@Component({  selector: 'app-stagger',  templateUrl: 'stagger.component.html',  styleUrls: ['stagger.component.css'],  animations: [    trigger('pageAnimations', [      transition(':enter', [        query('.item', [          style({opacity: 0, transform: 'translateY(-10px)'}),          stagger(200, [animate('500ms ease-in', style({opacity: 1, transform: 'none'}))]),        ]),      ]),    ]),  ],})export class StaggerComponent {  @HostBinding('@pageAnimations')  items = [1, 2, 3];}

src/app/stagger.component.html

      
<h2>Stagger Example</h2><ul class="items">  @for(item of items; track item) {    <li class="item">{{ item }}</li>  }</ul>

src/app/stagger.component.css

      
.items {  list-style: none;  padding: 0;  margin: 0;}

With Native CSS

src/app/stagger.component.ts

      
import {Component, signal} from '@angular/core';@Component({  selector: 'app-stagger',  templateUrl: './stagger.component.html',  styleUrls: ['stagger.component.css'],})export class StaggerComponent {  show = signal(true);  items = [1, 2, 3];  refresh() {    this.show.set(false);    setTimeout(() => {      this.show.set(true);    }, 10);  }}

src/app/stagger.component.html

      
<h1>Stagger Example</h1><button type="button" (click)="refresh()">Refresh</button>@if (show()) {  <ul class="items">    @for(item of items; track item) {      <li class="item" style="--index: {{ item }}">{{item}}</li>    }  </ul>}

src/app/stagger.component.css

      
.items {  list-style: none;  padding: 0;  margin: 0;}.items .item {  transition-property: opacity, transform;  transition-duration: 500ms;  transition-delay: calc(200ms * var(--index));  @starting-style {    opacity: 0;    transform: translateX(-10px);  }}

Parallel Animations

The animations package has a group() function to play multiple animations at the same time. In CSS, you have full control over animation timing. If you have multiple animations defined, you can apply all of them at once.

      
.target-element {  animation: rotate 3s, fade-in 2s;}

In this example, the rotate and fade-in animations fire at the same time.

Animating the items of a reordering list

Items reordering in a list works out of the box using the previously described techniques. No additional special work is required. Items in a @for loop will be removed and re-added properly, which will fire off animations using @starting-styles for entry animations. Removal animations will require additional code to add the event listener, as seen in the example above.

With Animations Package<

src/app/reorder.component.ts

      
import {Component, signal} from '@angular/core';import {trigger, transition, animate, query, style} from '@angular/animations';@Component({  selector: 'app-reorder',  templateUrl: './reorder.component.html',  styleUrls: ['reorder.component.css'],  animations: [    trigger('itemAnimation', [      transition(':enter', [        style({opacity: 0, transform: 'translateX(-10px)'}),        animate('300ms', style({opacity: 1, transform: 'translateX(none)'})),      ]),      transition(':leave', [        style({opacity: 1, transform: 'translateX(none)'}),        animate('300ms', style({opacity: 0, transform: 'translateX(-10px)'})),      ]),    ]),  ],})export class ReorderComponent {  show = signal(true);  items = ['stuff', 'things', 'cheese', 'paper', 'scissors', 'rock'];  randomize() {    const randItems = [...this.items];    const newItems = [];    for (let i of this.items) {      const max: number = this.items.length - newItems.length;      const randNum = Math.floor(Math.random() * max);      newItems.push(...randItems.splice(randNum, 1));    }    this.items = newItems;  }}

src/app/reorder.component.html

      
<h1>Reordering List Example</h1><button type="button" (click)="randomize()">Randomize</button><ul class="items">  @for(item of items; track item) {    <li @itemAnimation class="item">{{ item }}</li>  }</ul>

src/app/reorder.component.css

      
.items {  list-style: none;  padding: 0;  margin: 0;}

With Native CSS

src/app/reorder.component.ts

      
import {Component, signal} from '@angular/core';@Component({  selector: 'app-reorder',  templateUrl: './reorder.component.html',  styleUrls: ['reorder.component.css'],})export class ReorderComponent {  show = signal(true);  items = ['stuff', 'things', 'cheese', 'paper', 'scissors', 'rock'];  randomize() {    const randItems = [...this.items];    const newItems = [];    for (let i of this.items) {      const max: number = this.items.length - newItems.length;      const randNum = Math.floor(Math.random() * max);      newItems.push(...randItems.splice(randNum, 1));    }    this.items = newItems;  }}

src/app/reorder.component.html

      
<h1>Reordering List Example</h1><button type="button" (click)="randomize()">Randomize</button><ul class="items">  @for(item of items; track item) {    <li class="item">{{ item }}</li>  }</ul>

src/app/reorder.component.css

      
.items {  list-style: none;  padding: 0;  margin: 0;}.items .item {  transition-property: opacity, transform;  transition-duration: 500ms;  @starting-style {    opacity: 0;    transform: translateX(-10px);  }}

Migrating usages of AnimationPlayer

The AnimationPlayer class allows access to an animation to do more advanced things like pause, play, restart, and finish an animation through code. All of these things can be handled natively as well.

You can retrieve animations off an element directly using Element.getAnimations(). This returns an array of every Animation on that element. You can use the Animation API to do much more than you could with what the AnimationPlayer from the animations package offered. From here you can cancel(), play(), pause(), reverse() and much more. This native API should provide everything you need to control your animations.

Route Transitions

You can use view transitions to animate between routes. See the Route Transition Animations Guide to get started.