Creating a Staggered Animation for an Ionic Infinite List (without SASS)

There are many instances in which we can use a staggered animation in our applications, but it is perhaps most commonly used with lists or thumbnails/galleries. The idea is that we apply an animation (usually the same animation) to multiple elements, but we execute those animations with an increasing amount of delay for each element.

The result is an appearance that these individual animations are linked together, creating a kind of “flow” of elements animating on to the screen (or some other kind of animation). I recently published a video that walks through creating this effect in Ionic if you would like to see an example of what I am talking about:

There can be a practical purpose to this style of animation - in that it could be used to give a sense of direction (e.g. the list flows downwards and there is more content to discover below) or even as a mechanism to create “perceived performance” whilst loading - but for the most part, it just generally looks/feels nice (as long as the animation isn’t too slow).

The basic concept is simple enough, we create an animation:

@keyframes popIn {
  0% {
    opacity: 0;
    transform: scale(0.6) translateY(-8px);
  }

  100% {
    opacity: 1;
    transform: none;
  }
}

and then apply that animation to each element in our list:

ion-item {
  animation: popIn 0.2s both ease-in;
}

The trick is to add an animation-delay so that the animations don’t all trigger at once, e.g:

ion-item {
  animation: popIn 0.2s 70ms both ease-in;
}

The animation now has a delay of 70ms which means the browser will wait 70ms before starting the animation. However, this will apply the same delay to every item in the list, which is not what we want. We want the first ion-item to have no delay, the second to have 70ms of delay, the third to have 140ms of delay and so on.

Instead, we can do something like this:

ion-item:nth-child(1) {
  animation-delay: 70ms
}

ion-item:nth-child(2) {
  animation-delay: 140ms
}

ion-item:nth-child(3) {
  animation-delay: 210ms
}

This will achieve what we want, but you can see how this would quickly become tiresome to write and maintain, even for small lists. To make the process of writing out each of these rules above easier, it is common to use SASS which is a preprocessor for CSS that will allow us to write loops to create rules like the above automatically (e.g. we could create 50 nth-child rules with one loop in SASS).

This works, but there are some downsides. It is still a bit tricky to write out the loop, if you don’t know how many items will be in your list then SASS might create a bunch of unnecessary rules for items that don’t exist, or maybe you just don’t know how to use SASS or you don’t want to use it.

Staggered Animations without SASS

SASS was the way I would typically achieve staggered animations, but I started searching around for solutions that wouldn’t require any additional dependencies. As it turns out, it is possible to achieve this style of animation entirely with CSS.

In my research, I came across the following article by Daniel Benmore: Different Approaches for Creating a Staggered Animation. Daniel was searching for the same thing I was, and he came to a solution which I think is genius in its simplicity. We can do everything we need just with CSS Variables.

I’ve written a lot on the subject of CSS variables and Ionic before, so if you need a bit of background information you might be interested in the following articles:

The video I published above walks through adapting the concept Daniel talks about into an Ionic application with a list of a dynamic size. The example is created inside of an Ionic/StencilJS application, but the same concepts can be used with Angular, React, or Vue as well (just with a bit of tweaking for syntax).

One of the commenters on the video pointed out that the example with CSS variables would not work for an infinite list in Ionic. This is true, but with a small change the same concept can also be applied to infinite lists.

In this tutorial, I am going to walk through implementing the basic example that is already covered in the video, and then how we can modify that implementation so that it will work with an infinite list as well.

Before We Get Started

If you are following along with StencilJS, I will assume that you already have a basic understanding of how to use StencilJS. If you are following along with a framework like Angular, React, or Vue then you will need to adapt parts of this tutorial as we go.

If you would like a thorough introduction to building Ionic applications with StencilJS, you might be interested in checking out my book.

1. Animating a Static List

The key idea behind the CSS variable approach is that we attach a CSS variable to each element through the style attribute that represents its order in the list. The order of the element in the list is used to calculate its animation-delay - items later in the list will have larger animation delays. We name this CSS variable --animation-order in this example, but it could be named whatever you like.

Just as was the case with the SASS example, we could apply this manually to each element, e.g:

<ion-list>
  <ion-item style="--animation-order: 0">One</ion-item>
  <ion-item style="--animation-order: 1">Two</ion-item>
  <ion-item style="--animation-order: 2">Three</ion-item>
</ion-list>

But this is awkward and doesn’t work well with dynamic lists. What we can do instead is assign its value based on its position (index) in the array of values we are looping over for the list. In StencilJS (and React) that would look something like this:

  render() {
    return [
      <ion-header>
        <ion-toolbar color="primary">
          <ion-title>Home</ion-title>
        </ion-toolbar>
      </ion-header>,

      <ion-content>
        <ion-list lines="none">
          {this.items.map((item, index) => (
            <ion-item style={{ "--animation-order": index } as any}>
              <ion-label>{item}</ion-label>
            </ion-item>
          ))}
        </ion-list>

      </ion-content>
    ];
  }

We use the index in the loop to determine what the --animation-order should be. Since StencilJS uses TSX for the template (i.e. JSX with TypeScript) we also need to add as any since we don’t have a type for our CSS variable. In Angular, you would use an *ngFor loop instead of a map.

With an animation order assigned to each item in the list, the animation just becomes a matter of using that value to calculate the animation delay:

ion-item {
  animation: popIn 0.2s calc(var(--animation-order) * 70ms) both ease-in;
}

@keyframes popIn {
  0% {
    opacity: 0;
    transform: scale(0.6) translateY(-8px);
  }

  100% {
    opacity: 1;
    transform: none;
  }
}

The CSS above would give the element with an --animation-order of 0 an animation-delay of 0 * 70ms which is 0ms. The element with an --animation-order of 1 would have a delay of 1 * 70ms which is 70ms, and so on for each element in the list.

2. Animating an Infinite List

This method could work even for lists with 100s of items. The problem that an infinite list introduces is that not all of the items are loaded at the same time, instead, more items are loaded each time the user gets to the bottom of the list.

If we load in 10 items initially, and then another 10 when we get to the bottom of the list, we don’t want the 11th item in the list to have a delay 70ms longer than the 10th item - we want the 11th item to have no delay since we want it to animate on screen as soon as the additional items load, and then we want the 12th item to have 70ms of delay just like the 2nd item had. Basically, we want to reset the delays.

This issue sounds like it could be a bit tricky, but there is actually a very simple solution. All you need to do is use the following:

<ion-item style={{ "--animation-order": index % 10 } as any}>

We use the modulo operator (%) to return the remainder when the index is divided by 10 - you just need to change 10 to whatever your “page size” is (e.g. how many new items you load for each infinite load trigger). This can also be set dynamically. Let’s take the 12th item as an example. The 12th item in the list would have an index of 11 (since the index begins at 0). If we divide 11 by 10 (our page size) we will have a remainder of 1, which is what the modulo operator will return to us and use as the value for --animation-order.

Now when calculating the animation delay for the 12th item, we will have 1 * 70ms which is a delay of 70ms - exactly what we want. Here is an example with some dummy data loading in that you can try out for yourself (again, this is a StencilJS example):

import { Component, State, Listen, h } from "@stencil/core";

@Component({
  tag: "app-home",
  styleUrl: "app-home.css"
})
export class AppHome {
  @State() items: string[] = [];

  componentWillLoad() {
    this.items = [
      "one",
      "two",
      "three",
      "four",
      "five",
      "six",
      "seven",
      "eight",
      "nine",
      "ten"
    ];
  }

  @Listen("ionInfinite")
  loadData(event) {
    setTimeout(() => {
      console.log("Done");
      this.items.push(
        "one",
        "two",
        "three",
        "four",
        "five",
        "six",
        "seven",
        "eight",
        "nine",
        "ten"
      );
      this.items = [...this.items];
      event.target.complete();

      // App logic to determine if all data is loaded
      // and disable the infinite scroll
      if (this.items.length > 100) {
        event.target.disabled = true;
      }
    }, 1000);
  }

  render() {
    return [
      <ion-header>
        <ion-toolbar color="primary">
          <ion-title>Home</ion-title>
        </ion-toolbar>
      </ion-header>,

      <ion-content>
        <ion-list lines="none">
          {this.items.map((item, index) => (
            <ion-item style={{ "--animation-order": index % 10 } as any}>
              <ion-label>{item}</ion-label>
            </ion-item>
          ))}
        </ion-list>

        <ion-infinite-scroll threshold="100px">
          <ion-infinite-scroll-content
            loading-spinner="bubbles"
            loading-text="Loading more data..."
          ></ion-infinite-scroll-content>
        </ion-infinite-scroll>
      </ion-content>
    ];
  }
}

Summary

I am thrilled to have come across this method because I think that it is both more efficient and easier to execute than the SASS example, not to mention that it also doesn’t require installing and setting up SASS in your project. Thanks to Daniel Benmore for the fantastic concept!

Check out my latest videos: