Angular Router Animations Guide

Smooth animated router transitions will impress your users and set your app apart from the crowd. In the following lesson, you will learn how to build complex router transition animations in Angular.

Angular router animations demo

Here’s a summary of the animations that we will build from scratch.

  • fader basic fade in
  • slider slide in from left or right
  • transformer translate, scale, and rotate
  • stepper complex keyframed animation sequences

Router Animations Setup

Let’s start by taking care of the initial Angular Router animations boilerplate. Also, generate a few components so we have some content to route to.

Import the BrowserAnimationsModule

file_type_ng_component_ts app.module.ts
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
  declarations: [
    AppComponent,
    RightPageComponent,
    LeftPageComponent,
    AboutPageComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule // <-- here
    AppRoutingModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Wrap the Router Outlet

file_type_html app.component.html
<div [@routeAnimations]="prepareRoute(outlet)" >
  <router-outlet #outlet="outlet"></router-outlet>
</div>

The animations must be included in the app component. Uncomment the name of the animation you wish to use.

file_type_ng_component_ts foo.component.ts
@Component({
  selector: 'app-root',
  animations: [ // <-- add your animations here
    // fader,
    // slider,
    // transformer,
    stepper
  ]
})

prepareRoute(outlet: RouterOutlet) {
  return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation'];
}

Variable Animations with Router Data (Optional)

We can pass data into the router to customize animations on a component-by-component basis. For example, we might want apply different animations for components based on their perceived position in the UI.

file_type_ng_component_ts app-routing.module.ts
const routes: Routes = [
  { path: 'about', component: AboutPageComponent },
  { path: 'right', component: RightPageComponent, data: { animation: 'isRight' } },
  { path: 'left', component: LeftPageComponent, data: { animation: 'isLeft' } },
];

Create the Animations

Now it’s time for the fun part - animating. Let’s look at four animation examples that become increasingly more complex after each step.

Create a file named route-animations.ts in the app dir.

file_type_ng_component_ts route-animations.ts
import {
    trigger,
    transition,
    style,
    query,
    group,
    animateChild,
    animate,
    keyframes,
  } from '@angular/animations';

Basic Fade In

Let’s start with a basic animation that is applied the same for every route.

A few important notes about the code below:

  • The trigger name of routeAnimations must match the trigger used in the app component HTML.
  • Using wildcard syntax like * <=> * applies the default animation to all routes.
  • When an animation is triggered we have access the the previous page via the :leave selector, and the current page via the :enter selector. We can query these elements to style and animate them.

The animation below first hides both pages, then defines an animation to fade in the new page.

file_type_ng_component_ts foo.component.ts
export const fader =
  trigger('routeAnimations', [
    transition('* <=> *', [
      // Set a default  style for enter and leave
      query(':enter, :leave', [
        style({
          position: 'absolute',
          left: 0,
          width: '100%',
          opacity: 0,
          transform: 'scale(0) translateY(100%)',
        }),
      ]),
      // Animate the new page in
      query(':enter', [
        animate('600ms ease', style({ opacity: 1, transform: 'scale(1) translateY(0)' })),
      ])
    ]),
]);

Slide in from Left or Right

In this example, we will write a function that can be reused to generate an animation for both left and right directions.

  • We set different transitions based on the isLeft or isRight data contiained in the router.
  • :leave queries are marked optional because it is not present on the initial page load
  • The group method is used run multiple animations concurrently.
export const slider =
  trigger('routeAnimations', [
    transition('* => isLeft', slideTo('left') ),
    transition('* => isRight', slideTo('right') ),
    transition('isRight => *', slideTo('left') ),
    transition('isLeft => *', slideTo('right') )
  ]);

function slideTo(direction) {
  const optional = { optional: true };
  return [
    query(':enter, :leave', [
      style({
        position: 'absolute',
        top: 0,
        [direction]: 0,
        width: '100%'
      })
    ], optional),
    query(':enter', [
      style({ [direction]: '-100%'})
    ]),
    group([
      query(':leave', [
        animate('600ms ease', style({ [direction]: '100%'}))
      ], optional),
      query(':enter', [
        animate('600ms ease', style({ [direction]: '0%'}))
      ])
    ]),
    // Normalize the page style... Might not be necessary

    // Required only if you have child animations on the page
    // query(':leave', animateChild()),
    // query(':enter', animateChild()),
  ];
}

Configurable CSS Transformations

Rather than animating the right or left style attributes, we can achieve better flexibility and control with a transform. This demo is similar to the previous sliding animation, but adds a rotation to make it drop and spin in from the top corner. Tweak the named arguments to easily modify its behavior.

export const transformer =
  trigger('routeAnimations', [
    transition('* => isLeft', transformTo({ x: -100, y: -100, rotate: -720 }) ),
    transition('* => isRight', transformTo({ x: 100, y: -100, rotate: 90 }) ),
    transition('isRight => *', transformTo({ x: -100, y: -100, rotate: 360 }) ),
    transition('isLeft => *', transformTo({ x: 100, y: -100, rotate: -360 }) )
]);


function transformTo({x = 100, y = 0, rotate = 0}) {
  const optional = { optional: true };
  return [
    query(':enter, :leave', [
      style({
        position: 'absolute',
        top: 0,
        left: 0,
        width: '100%'
      })
    ], optional),
    query(':enter', [
      style({ transform: `translate(${x}%, ${y}%) rotate(${rotate}deg)`})
    ]),
    group([
      query(':leave', [
        animate('600ms ease-out', style({ transform: `translate(${x}%, ${y}%) rotate(${rotate}deg)`}))
      ], optional),
      query(':enter', [
        animate('600ms ease-out', style({ transform: `translate(0, 0) rotate(0)`}))
      ])
    ]),
  ];
}

Keyframe Sequence

Keyframes allow us build an animation in multiple complex steps. The effect below makes it look like the new page pops up from the bottom, then bumps the old page off the screen in explosive fashion. The offset property is used by the Angular keyframe function determine when (as a percent of the total animation time) a style will be applied. In brief, keyframes allow us to compose complex sequences of style changes.

export const stepper =
  trigger('routeAnimations', [
    transition('* <=> *', [
      query(':enter, :leave', [
        style({
          position: 'absolute',
          left: 0,
          width: '100%',
        }),
      ]),
      group([
        query(':enter', [
          animate('2000ms ease', keyframes([
            style({ transform: 'scale(0) translateX(100%)', offset: 0 }),
            style({ transform: 'scale(0.5) translateX(25%)', offset: 0.3 }),
            style({ transform: 'scale(1) translateX(0%)', offset: 1 }),
          ])),
        ]),
        query(':leave', [
          animate('2000ms ease', keyframes([
            style({ transform: 'scale(1)', offset: 0 }),
            style({ transform: 'scale(0.5) translateX(-25%) rotate(0)', offset: 0.35 }),
            style({ opacity: 0, transform: 'translateX(-50%) rotate(-180deg) scale(6)', offset: 1 }),
          ])),
        ])
      ]),
    ])

]);

Questions? Let's chat

Open Discord