Animating Route Changes in Angular

Angular 4.2 brought us a huge array of new animation features. We’ll cover a lot of those new features in subsequent posts, but here we’ll focus one one very interesting new capability: animating the transition between routes.

You'll want to make sure that you're running Angular 4.2+ to implement router state animations. If you're new to animations in Angular, you may want to refer to this post first.

App Setup

First, let’s hook-up the app module so that we can use Angular’s animation module:

app.module.ts

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

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

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

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

Let’s also setup a few routes in an routing module for demonstration:

app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { OneComponent } from './one.component';
import { TwoComponent } from './two.component';
import { ThreeComponent } from './three.component';

const routes: Routes = [
{ path: '', redirectTo: 'one', pathMatch: 'full' },
{ path: 'one', component: OneComponent, data: { page: 'one' } },
{ path: 'two', component: TwoComponent, data: { page: 'two' } },
{ path: 'three', component: ThreeComponent, data: { page: 'three' } }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

Notice here that we added some data to each route, which will come in handy to indicate to the animation system when there’s a route state change.

Animations Setup

Now that our routes are defined and that the Angular animations module is available, we can start setting up the animations, which will revolve around the app’s router-outlet.

First, we’ll wrap the router outlet in a div that’ll be wrapping around of routed components:

app.component.html

<div class="main" [@animRoutes]="getPage(appOutlet)">
  <router-outlet #appOutlet="outlet"></router-outlet>
</div>

We’re exporting the router outlet as appOutlet and then in the wrapping div we specify an animation trigger called animRoutes that gets its value from a getPage method that takes the exported outlet. Here’s what our getPage method looks like:

app.component.ts

getPage(outlet) {
  return outlet.activatedRouteData['page'] || 'one';
}

Our method returns the page value for the current route as we defined in our routes. Route changes will trigger a change in the returned value.

A bit of styles

We gave a class of main to our wrapping div in the app component so that we can give it some styles:

app.component.css

.main {
  position: relative;
  width: 35vw;
  height: 35vh;
  margin: 0 auto;
}

And our individual routed components will be styled with something like this:

one.component.css

:host {
  width: 100%;
  height: 100%;
  background: #5942f4;
  display: flex;
  justify-content: center;
  align-items: center;
  color: white;
  font-family: Bangers;
  font-size: 2rem;
  margin: 3rem auto;

  position: absolute;
}

With these styles, each routed component will occupy the same space of 35vw by 35vh at the center of the page.

Animation Definitions

Now that everything is in place, we can define the actual animation happening between each route change. First let’s import a few members from @angular/animations:

app.component.ts

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

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  animations: [
    // ...animation definitions here
  ]
})
export class AppComponent {
  getPage(outlet) {
    return outlet.activatedRouteData['page'] || 'one';
  }
}

And here are the animation definitions that go in the app component’s decorator:

app.component.ts

// ...imports

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  animations: [
    trigger('animRoutes', [
      transition('* <=> *', [
        group([
          query(
            ':enter',
            [
              style({
                opacity: 0,
                transform: 'translateY(9rem) rotate(-10deg)'
              }),
              animate(
                '0.35s cubic-bezier(0, 1.8, 1, 1.8)',
                style({ opacity: 1, transform: 'translateY(0) rotate(0)' })
              ),
              animateChild()
            ],
            { optional: true }
          ),
          query(
            ':leave',
            [animate('0.35s', style({ opacity: 0 })), animateChild()],
            { optional: true }
          )
        ])
      ])
    ])
  ]
})
export class AppComponent {
  // ...
}

It may seam like a lot to digest at first, so let’s explain a few of the important parts:

  • We define everything under our animRoutes trigger that was applied to the div that wraps the router outlet in the app component template.
  • Then we define a transition for any state to any state (’* <=> *’) so that every route change yields the same animation. We could define multiple transitions for different states. For example, from page one to page two: ‘one => two’, or from page two to page one: ‘two => one’.
  • Next we use two query groups to grab anything that’s entering or leaving the DOM, respectively.
  • We style the :enter state with a starting opacity of 0 and some rotate/translateY transforms, and then animate in to the full opacity and our transforms back to the initial values. We also give our animation a custom cubic-bezier easing for an effect where we get a little bit of a bounce.
  • Components that are leaving the DOM get a more subtle animation will only their opacity going down to 0.
  • We add { optional: true } to our query groups so that Angular doesn’t trip up when there’s nothing to query for.
  • We also add calls to the animateChild method as the last item to the array of instructions passed to our query groups. This will trigger any inner animations after the main animation is complete.

Here's the result of our route changes:

Example of route change animation

✖ Clear

🕵 Search Results

🔎 Searching...