Scroll to top on Angular Router navigation

Scroll to top on Angular Router navigation

Posted on
Last update on

Update (December 2018): This article has been updated to represent the newly available ViewportScroller class, available from Angular v7+. This class implementation wraps around the window object and only executes if the window object is available.

Update (July 2019): This article has been updated to use the proper configuration on the RouterModule to perform a scroll to top on navigation and preserve the previous scroll position. See the last section of this article.

When I was creating this blog and optimising it for mobile I experienced some default but not so user-friendly behaviour when navigating from one route to the other with Angular.

The problem is that content on mobile can go very deep below the initial viewport height. So when you're scrolling down and you press an internal link to another page, you'll be stuck at that height.

This is somewhat different from standard navigation between pages in a normal web-application, where the page reloads and you start from the top by default. In a S.P.A. this can easily be solved by scrolling to the top on navigation by using the native window.scroll function:

window.scroll(0,0)

A navigation in routing in Angular 1 and ngRoute or even the ui-router can easily be detected by listening to the event $routeChangeSuccess or $stateChangeSuccess. So combining these 2 essentials gives us:

angular-js-route-state-change.ts

// ngRoute:
$rootScope.$on('$routeChangeSuccess', () => {
    $window.scroll(0,0);
});

// ui-router:
$rootScope.$on('$stateChangeSuccess', () => {
    $window.scroll(0,0);
});

I did not find anything similar in the documentation of the router of Angular so I went digging. The fact is that I'm using the Angulartics2 plugin by @luisfarzati to track you guys' behavior :). This is also done on navigation so there must be something similar going on at that plugin. The plugin BTW works great!

Listening to navigation events in Angular

It seems that the Angular v2+ router has an events Observable property which you can subscribe on. Yes, it is as simple as that. Those events can be of any predefined type NavigationStart, NavigationCancel, NavigationEnd or NavigationError. In my case I only needed the NavigationEnd.

In the component that holds your navigation router-outlet you just need to setup the listener, something like this:

app.component.ts

import { Component } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { ViewportScroller } from '@angular/common';

@Component({
  selector: 'sv-app',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent {

  constructor(
    readonly router: Router,
    readonly viewportScroller: ViewportScroller
  ) {

    router.events
      .filter(event => event instanceof NavigationEnd)
      .subscribe((event: NavigationEnd) => {
        // Angular v2-v6
        window.scroll(0, 0);
        // Angular v7+
        this.viewportScroller.scrollToPosition([0, 0]);
      });

  }
}

Update (December 2018): This article has been updated to represent the newly available ViewportScroller class, available from Angular v7+. This class implementation wraps around the window object and only executes if the window object is available.

Update (July 2019): This article has been updated to use the proper configuration on the RouterModule to perform a scroll to top on navigation and preserve the previous scroll position.

Using the scrollPositionRestoration config option of the RouterModule

Since v6.1 it is possible to set a configuration option scrollPositionRestoration on the RouterModule that will preserve the scrolling position of the previous route and scroll to top on a succesful navigation to the new route.

app-routin.module.ts

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

const routes: Routes = [
  {
    path: '',
    loadChildren: './pages/home/home.module#HomeModule',
  },
  // ..
];

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

This is currently implemented on my blog as you can see. And with a simple style setting on the HTML this performs a smooth scroll whenever there is the need to!

styles.scss

html {
    scroll-behavior: smooth;
}

Visual example

In the visual example below I first scroll down on the projects page. Next I navigate to the posts overview. During this navigation, the window is scrolled to top automatically. When I hit back in the browser, the application is again focussed on the scrolling position I was before.

Conclusion

And that was it! Be aware that the window object might not be available in every context, except the browser. Check this awesome article by @juristr to read more about why you might want to wrap your window object reference!

Please also be careful not to use these events to do business logic, like for example checking if you can navigate to a specific route based on some authentication rules. For those cases you may want to implement guards! More information about guards can be found in this splendid article by @PascalPrecht of Thoughtram.

Contents

  1. Listening to navigation events in Angular
  2. Using the scrollPositionRestoration config option of the RouterModule
  3. Visual example
  4. Conclusion

By reading this article I hope you can find a solution for your problem. If it still seems a little bit unclear, you can hire me for helping you solve your specific problem or use case. Sometimes even just a quick code review or second opinion can make a great difference.