Angular Animations: How to animate lists blog post

Angular Animations: How to animate lists

Cory Rylan

11 Sep, 2019

Angular

8 minutes read

The Angular Animations API provides a declarative API to build and reuse animations throughout our components. In this post, we will learn how to make simple transition animation and then build on what we learned to make a complex animation for a text and image-based lists using *ngFor.

First, let’s start with a creating a basic fade animation when something is show or hidden from view. Feel free to have a click around the below StackBlitz example to see what you’re about to learn!

Using a toggle with *ngIf we show and hide a text block based on a boolean condition in our component.

@Component({
  selector: 'my-app',
  template: `
    <button (click)="show = !show">toggle</button>
    <h1 *ngIf="show">Angular Animations</h1>
  `
})
export class AppComponent  {
  show = true;
}

With *ngIf the element will be removed entirely from the DOM when the show property is false and added back when true. The *ngIf will run when our animation completes.

Adding an Animation

To add a simple fade animation to our toggle example, we need to import the Angular Animations module into our application.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { AppComponent } from './app.component';

@NgModule({
  imports:      [BrowserModule, BrowserAnimationsModule],
  declarations: [AppComponent],
  bootstrap:    [AppComponent]
})
export class AppModule { }

Now that we have the BrowserAnimationsModule imported, we can start creating our animation. To create our first animation, we will add a new property to our component decorator, animations.

@Component({
  selector: 'my-app',
  template: `
    <button (click)="show = !show">toggle</button>
    <h1 *ngIf="show">Angular Animations</h1>
  `,
  animations: []
})
export class AppComponent  {
  show = true;
}

The animations property can take one to many animations we would like to use in our component template. We define our animation in a variable so we can use it in multiple places in our application.

import { trigger, transition, style, animate, query } from '@angular/animations';

export const fadeAnimation = trigger('fadeAnimation', [
  transition(':enter', [
    style({ opacity: 0 }), animate('300ms', style({ opacity: 1 }))]
  ),
  transition(':leave',
    [style({ opacity: 1 }), animate('300ms', style({ opacity: 0 }))]
  )
]);

Let’s walk through this animation step by step. First is the trigger function. The trigger takes two parameters, first the name of the animation we refer to in our template. The second parameter we provide the animation metadata or definition describing how the animation should behave.

In the animation, we can define a transition and how that transition is triggered. In this example, our transition function first parameter is :enter. The first parameter describes that our transition will run whenever something enters the DOM (example using *ngIf).

In our transition, we can write styles and animations that we want to be applied when the transition occurs. In our :enter state we set the element to have an opacity of 0 and then use the animate function to animate it to an opacity of 1 over 300ms.

Our second transition :leave is similar to the first but inverted in behavior. When the element leaves or is removed from the DOM, we animate it back to 0 opacity over 300ms. To use this animation, we add it to the animations list in the component decorator and add a reference in the template.

import { Component } from '@angular/core';
import { trigger, transition, style, animate, query } from '@angular/animations';

...

@Component({
  selector: 'my-app',
  template: `
    <button (click)="show = !show">toggle</button>
    <h1 @fadeAnimation *ngIf="show">Angular Animations</h1>
  `,
  animations: [fadeAnimation]
})
export class AppComponent  {
  show = true;
}

To reference an animation to an element, we add the name of the animation prefixed with a @ on the element we would like to animate. For this example, the animation name in the template will be @fadeAnimation. Now that we have a straightforward transition animation working, let’s move onto creating a list animation.

Animating Lists with ngFor

In this next example, we will build a basic text list and animate each item.

First, let’s build out a dynamic list with *ngFor.

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <button (click)="toggleList()">toggle list</button>

    <ul class="items">
      <li *ngFor="let item of items" class="item">
        Item {{item}}
      </li>
    </ul>
  `,
  animations: [fadeAnimation]
})
export class AppComponent  {
  items = [];

  toggleList() {
    this.items = this.items.length ? [] : [0,1,2,3,4,5,6,7,8,9,10];
  }
}

Whenever we click our toggle button, we add or remove ten items from our list. When we animate our list, we don’t show all items all at once but wait until the previous list item is visible before starting the next item animation. This delay technique is called staggering. To stagger the list items, our syntax for the animation changes a bit.

import { trigger, transition, style, animate, query, stagger } from '@angular/animations';

const listAnimation = trigger('listAnimation', [
  transition('* <=> *', [
    query(':enter',
      [style({ opacity: 0 }), stagger('60ms', animate('600ms ease-out', style({ opacity: 1 })))],
      { optional: true }
    ),
    query(':leave',
      animate('200ms', style({ opacity: 0 })),
      { optional: true }
    )
  ])
]);

In this example, we have a single transition function with the first parameter as * <=> *. This syntax triggers the animation when any value passed into the trigger via the template has changed. This syntax can be used for when specific values have changed, read more in the documentation. We will refer back to this syntax when we get to the template code. The query function allows us to query child elements and trigger an animation when child element leaves or enters our list.

Like before when an item enters, we use the :enter syntax and run our animation. We start the item with a 0 opacity nad then apply our opacity 1 style. In this example, we use the stagger function which will delay the animation from running until a given time frame after the previous item has finished animating. This creates the nice stagger animation on the list items we see in the video clip.

The last parameter for our query is a configuration object { optional: true }. This option tells the query animation not to throw an error if the element does not exist when the animation is triggered in the view.

In our :leave query we could stagger the items again but for a better user experience we quickly fade away all the items for the user so we can start to render the next list.

To add our animation to our component, we need to add the trigger to the template.

<ul [@listAnimation]="items.length" class="items">
  <li *ngFor="let item of items" class="item">
    Item {{item}}
  </li>
</ul>

Notice how the @listAnimation takes the items.length as a property value. This binding is important and the reason why we use the * <=> * syntax we saw earlier. Any time the binding value changes on the trigger our animation re-runs and executes our animation queries.

Because we are using the query and stagger functions to delay each item animation the trigger @listAnimation needs to be on a parent element. In this case, the parent element is the ul element. Now that we have a basic list working, we can make things a little more interesting by using images and use the animation we just created.

import { Component } from '@angular/core';
import { trigger, transition, style, animate, query, stagger } from '@angular/animations';

const listAnimation = trigger('listAnimation', [
  transition('* <=> *', [
    query(':enter',
      [style({ opacity: 0 }), stagger('60ms', animate('600ms ease-out', style({ opacity: 1 })))],
      { optional: true }
    ),
    query(':leave',
      animate('200ms', style({ opacity: 0 })),
      { optional: true}
    )
  ])
]);

const images = [
  'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
  'https://media.giphy.com/media/6C4y1oxC6182MsyjvK/giphy.gif',
  'https://media.giphy.com/media/Ov5NiLVXT8JEc/giphy.gif',
  'https://media.giphy.com/media/SRO0ZwmImic0/giphy.gif',
  'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif'
];

@Component({
  selector: 'my-app',
  template: `
  <button (click)="toggleImages()">toggle images</button>
  <section [@listAnimation]="images.length" class="images">
    <img *ngFor="let img of images" [src]="img" class="image" />
  </section>
  `,
  animations: [fadeAnimation]
})
export class AppComponent  {
  images = [];

  toggleImages() {
    this.images = this.images.length ? [] : images;
  }
}

By applying the same animation over a list of images, we can get a nice effect when loading the list of images.

Notice how the animation applies in order of the items in the list and not the order they are rendered in DOM by rows.

About the author

Cory Rylan profile picture

Cory Rylan

GDE Google Developer Expert

Cory Rylan is a Google Developer Expert and Front End Developer for VMware Clarity. He enjoys building fast progressive web applications and reusable design systems with Web Components and Angular. He also enjoys teaching via workshops, conference speaking, and writing.

Love the post? Share it!

Lots of time and effort go into all our blogs, resources and demos,
we'd love it if you'd spare a moment to share them!

Explore our Angular courses

Get started today and join over 50,000 developers.