angularanimationtransitionangular-routerangular-animations

How to add fade-in animation to Angular <router-outlet> on route change?


I have html component:

<section
  class="flex flex-col w-full h-[100vh] overflow-hidden animate__animated animate__fadeIn"
>
  <!-- Top navbar remains fixed at the top and does not shrink -->
  <app-top-navbar class="flex-shrink-0"></app-top-navbar>

  <!-- Main content area with side navbar and dynamic content -->
  <div class="h-full flex overflow-hidden">
    <!-- Side navbar remains fixed in width and does not grow or shrink -->
    <app-side-navbar class="w-[10%] flex-shrink-0"></app-side-navbar>

    <!-- Content area that can expand and scroll independently -->
    <div class="w-full flex flex-col relative h-full overflow-y-auto">
      <p class="p-3 drop-shadow-md text-gray-800 font-semibold">
        {{ sectionTitle }}
      </p>
      <!-- Centered router-outlet container that can grow vertically -->
      <div class="flex items-center justify-center h-full">
        <router-outlet></router-outlet>
      </div>

      <!-- Bottom navigation positioned at the bottom of the content area -->
      <app-overview-nav
        [back_router_link]="back_router_link"
        [next_router_link]="next_router_link"
      ></app-overview-nav>
    </div>
  </div>
</section>

when it is loaded, the whole page is animated using fadeIn animation. But i want to animate also

<div class="flex items-center justify-center h-full">
            <router-outlet></router-outlet>
          </div>

so every time the router outlet changes, it also has animate__animated animate__fadeIn animation.

I tried using animate__animated animate__fadeIn on router-outlet, but it doesnt work.


Solution

  • You can check out this amazing tutorial, that explains the concept clearly.

    Angular Router Fade Animation

    We need to import animations using provideAnimations().

    We define the animation to be used, here it's fade animation.

    export const fadeAnimation = trigger('fadeAnimation', [
      transition('* => *', [
        query(':enter', [style({ opacity: 0, position: 'absolute' })], {
          optional: true,
        }),
        query(
          ':leave',
          [
            style({ opacity: 1 }),
            animate('0.3s', style({ opacity: 0, position: 'absolute' })),
          ],
          { optional: true }
        ),
        query(
          ':enter',
          [
            style({ opacity: 0 }),
            animate('0.3s', style({ opacity: 1, position: 'relative' })),
          ],
          { optional: true }
        ),
      ]),
    ]);
    

    Then, we can add the animation decorator [@fadeAnimation], this takes an input of when to trigger the animation, for this, we are using a reference to the router-outlet, using template reference variables. Then if it is present then the animation will fire.

      <main role="main" [@fadeAnimation]="outlet.isActivated ? outlet.activatedRoute : ''">
        <router-outlet #outlet="outlet"></router-outlet>
      </main>
    

    Full Code:

    import { Component } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import { provideAnimations } from '@angular/platform-browser/animations';
    import { RouterModule, provideRouter } from '@angular/router';
    import {
      trigger,
      animate,
      transition,
      style,
      query,
    } from '@angular/animations';
    
    export const fadeAnimation = trigger('fadeAnimation', [
      transition('* => *', [
        query(':enter', [style({ opacity: 0, position: 'absolute' })], {
          optional: true,
        }),
        query(
          ':leave',
          [
            style({ opacity: 1 }),
            animate('0.3s', style({ opacity: 0, position: 'absolute' })),
          ],
          { optional: true }
        ),
        query(
          ':enter',
          [
            style({ opacity: 0 }),
            animate('0.3s', style({ opacity: 1, position: 'relative' })),
          ],
          { optional: true }
        ),
      ]),
    ]);
    
    @Component({
      selector: 'app-a',
      standalone: true,
      template: `
        <h1>Hello from {{ name }}!</h1>
        <a target="_blank" href="https://angular.dev/overview">
          Learn more about Angular
        </a>
      `,
    })
    export class A {
      name = 'Angular';
    }
    
    @Component({
      selector: 'app-b',
      standalone: true,
      template: `
        <h1>Hello from {{ name }}!</h1>
        <a target="_blank" href="https://angular.dev/overview">
          Learn more about Angular
        </a>
      `,
    })
    export class B {
      name = 'Angular';
    }
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [RouterModule, A, B],
      animations: [fadeAnimation],
      template: `
        <div class="container">
          <header class="my-3">
            <ul class="nav nav-pills">
              <li class="nav-item">
                <a class="nav-link" [routerLink]="['/a']" routerLinkActive="active"
                  [routerLinkActiveOptions]="{ exact: true }">Home</a>
              </li>
              <li class="nav-item">
                <a class="nav-link" [routerLink]="['/b']" routerLinkActive="active">About</a>
              </li>
            </ul>
          </header>
    
          <main role="main" [@fadeAnimation]="outlet.isActivated ? outlet.activatedRoute : ''">
            <router-outlet #outlet="outlet"></router-outlet>
          </main>
        </div>
      `,
    })
    export class App {
      name = 'Angular';
    }
    
    bootstrapApplication(App, {
      providers: [
        provideAnimations(),
        provideRouter([
          { path: 'a', component: A },
          { path: 'b', component: B },
        ]),
      ],
    });
    

    Stackblitz Demo