Angular 2 Scroll to top on Route Change

222,711

Solution 1

Angular 6.1 and later:

Angular 6.1 (released on 2018-07-25) added built-in support to handle this issue, through a feature called "Router Scroll Position Restoration". As described in the official Angular blog, you just need to enable this in the router configuration like this:

RouterModule.forRoot(routes, {scrollPositionRestoration: 'enabled'})

Furthermore, the blog states "It is expected that this will become the default in a future major release". So far this hasn't happened (as of Angular 11.0), but eventually you won't need to do anything at all in your code, and this will just work correctly out of the box.

You can see more details about this feature and how to customize this behavior in the official docs.

Angular 6.0 and earlier:

While @GuilhermeMeireles's excellent answer fixes the original problem, it introduces a new one, by breaking the normal behavior you expect when you navigate back or forward (with browser buttons or via Location in code). The expected behavior is that when you navigate back to the page, it should remain scrolled down to the same location it was when you clicked on the link, but scrolling to the top when arriving at every page obviously breaks this expectation.

The code below expands the logic to detect this kind of navigation by subscribing to Location's PopStateEvent sequence and skipping the scroll-to-top logic if the newly arrived-at page is a result of such an event.

If the page you navigate back from is long enough to cover the whole viewport, the scroll position is restored automatically, but as @JordanNelson correctly pointed out, if the page is shorter you need to keep track of the original y scroll position and restored it explicitly when you go back to the page. The updated version of the code covers this case too, by always explicitly restoring the scroll position.

import { Component, OnInit } from '@angular/core';
import { Router, NavigationStart, NavigationEnd } from '@angular/router';
import { Location, PopStateEvent } from "@angular/common";

@Component({
    selector: 'my-app',
    template: '<ng-content></ng-content>',
})
export class MyAppComponent implements OnInit {

    private lastPoppedUrl: string;
    private yScrollStack: number[] = [];

    constructor(private router: Router, private location: Location) { }

    ngOnInit() {
        this.location.subscribe((ev:PopStateEvent) => {
            this.lastPoppedUrl = ev.url;
        });
        this.router.events.subscribe((ev:any) => {
            if (ev instanceof NavigationStart) {
                if (ev.url != this.lastPoppedUrl)
                    this.yScrollStack.push(window.scrollY);
            } else if (ev instanceof NavigationEnd) {
                if (ev.url == this.lastPoppedUrl) {
                    this.lastPoppedUrl = undefined;
                    window.scrollTo(0, this.yScrollStack.pop());
                } else
                    window.scrollTo(0, 0);
            }
        });
    }
}

Solution 2

You can register a route change listener on your main component and scroll to top on route changes.

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

@Component({
    selector: 'my-app',
    template: '<ng-content></ng-content>',
})
export class MyAppComponent implements OnInit {
    constructor(private router: Router) { }

    ngOnInit() {
        this.router.events.subscribe((evt) => {
            if (!(evt instanceof NavigationEnd)) {
                return;
            }
            window.scrollTo(0, 0)
        });
    }
}

Solution 3

From Angular 6.1, you can now avoid the hassle and pass extraOptions to your RouterModule.forRoot() as a second parameter and can specify scrollPositionRestoration: enabled to tell Angular to scroll to top whenever the route changes.

By default you will find this in app-routing.module.ts:

const routes: Routes = [
  {
    path: '...'
    component: ...
  },
  ...
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      scrollPositionRestoration: 'enabled', // Add options right here
    })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Angular Official Docs

Solution 4

You can write this more succinctly by taking advantage of the observable filter method:

this.router.events.filter(event => event instanceof NavigationEnd).subscribe(() => {
      this.window.scrollTo(0, 0);
});

If you're having issues scrolling to the top when using the Angular Material 2 sidenav this will help. The window or document body won't have the scrollbar so you need to get the sidenav content container and scroll that element, otherwise try scrolling the window as a default.

this.router.events.filter(event => event instanceof NavigationEnd)
  .subscribe(() => {
      const contentContainer = document.querySelector('.mat-sidenav-content') || this.window;
      contentContainer.scrollTo(0, 0);
});

Also, the Angular CDK v6.x has a scrolling package now that might help with handling scrolling.

Solution 5

Angular lately introduced a new feature, inside angular routing module make changes like below

@NgModule({
  imports: [RouterModule.forRoot(routes,{
    scrollPositionRestoration: 'top'
  })],
Share:
222,711
Naveed Ahmed
Author by

Naveed Ahmed

Updated on July 29, 2022

Comments

  • Naveed Ahmed
    Naveed Ahmed almost 2 years

    In my Angular 2 app when I scroll down a page and click the link at the bottom of the page, it does change the route and takes me to the next page but it doesn't scroll to the top of the page. As a result, if the first page is lengthy and 2nd page has few contents, it gives an impression that the 2nd page lacks the contents. Since the contents are visible only if a user scrolls to the top of the page.

    I can scroll the window to the top of the page in ngInit of the component but, is there any better solution that can automatically handle all routes in my app?

  • Naveed Ahmed
    Naveed Ahmed over 7 years
    Thank you so much @Guilherme does this approach has any performance implications? Since this subscription will last throughout app life.
  • Guilherme Meireles
    Guilherme Meireles over 7 years
    It's just one subscription that is triggered only on route events. It should have very little or no performance impact. Just make sure to put it only in the main component of the application. If you decide to use somewhere else unsubscribe to the events when the component is destroyed to avoid leaks.
  • Naveed Ahmed
    Naveed Ahmed over 7 years
    @Diego did you make any change to the answer?
  • Diego Unanue
    Diego Unanue over 7 years
    @NaveedAhmed just remove the snippet, because it is code that not runs in a snippet, not the answer itself, and the editing goes through moderators please read Stack Overflow meta.stackexchange.com/questions/21788/how-does-editing-work
  • Wowbagger and his liquid lunch
    Wowbagger and his liquid lunch over 7 years
    window.scrollTo(0, 0) is a more concise than document.body.scrollTop = 0;, and more readable IMO.
  • rgk
    rgk over 7 years
    Did anybody noticed, that even after implementing this, issue persists in safari browser of Iphone. any thoughts?
  • KCarnaille
    KCarnaille about 7 years
    @mehaase Looks like your answer is the best one. window.body.scrollTop doesn't work for me on Firefox desktop. So thank you !
  • tubbsy
    tubbsy about 7 years
    in my case it also required the page wrapping element css to be set to; height: 100vh + 1px;
  • Amir Tugi
    Amir Tugi almost 7 years
    Great! For me that worked - document.querySelector('.mat-sidenav-content .content-div').scrollTop = 0;
  • JackKalish
    JackKalish almost 7 years
    This worked for me, but it breaks the default "back" button behavior. Going back should remember the previous scroll position.
  • Moshe
    Moshe almost 7 years
    does this go in app.component.ts or every component.ts which is part of my main router-outlet?
  • Fernando Echeverria
    Fernando Echeverria almost 7 years
    This should go either in the app component directly, or in a single component used in it (and therefore shared by the whole app). For instance, I've included it in a top navigation bar component. You should not included in all your components.
  • Moshe
    Moshe almost 7 years
    I ended up putting this on my app.component. Question regarding "window," there are many articles and blogs recommending to use window as an Injectable service (window service), something with zone.js and "angular's consciousness"? Should I wrap window around a service?
  • Fernando Echeverria
    Fernando Echeverria almost 7 years
    You can do that and it will make the code more widely compatible with other, non-browser, platforms. See stackoverflow.com/q/34177221/2858481 for implementation details.
  • Manubhargav
    Manubhargav almost 7 years
    This worked!! Although I added $("body").animate({ scrollTop: 0 }, 1000); rather than window.scrollTo(0, 0) to animate smooth scrolling to top
  • Tim Harker
    Tim Harker almost 7 years
    Nice one fellas... at mtpultz & @AmirTugi. Dealing with this right now, and you nailed it for me, cheers! Probably will inevitably end up rolling my own side nav since Material 2's doesn't play nice when md-toolbar is position:fixed (at top). Unless you guys have ideas....????
  • Tim Harker
    Tim Harker almost 7 years
    Might have found my answer... stackoverflow.com/a/40396105/3389046
  • Victor Bredihin
    Victor Bredihin over 6 years
    @GuilhermeMeireles what's about situation when route is changing on the same page? like with <nav md-tab-nav-bar>
  • adamdport
    adamdport over 6 years
    If you click and hold the back/forward button in modern browsers, a menu appears that lets you navigate to locations other than your immediately previous/next one. This solution breaks if you do that. It's an edge case for most, but worth mentioning.
  • Poul Kruijt
    Poul Kruijt over 6 years
    Don't you need to inject PLATFORM_ID in the constructor and give this value as parameter in de isPlatformBrowser method?
  • Lazar Ljubenović
    Lazar Ljubenović over 6 years
    @PierreDuc Yes, the answer is wrong. isPlatformBrowser is a function and will always be truthy. I've edited it now.
  • Raptor
    Raptor over 6 years
    Thanks! It's correct now! Just verified the API: github.com/angular/angular/blob/…
  • Henrique César Madeira
    Henrique César Madeira about 6 years
    Why not: if ( evt instanceof NavigationEnd ) { window.scrollTo(0, 0); }
  • CodyBugstein
    CodyBugstein about 6 years
    pardon my laziness in not bothering to learn pug, but can you translate to HTML?
  • Sal_Vader_808
    Sal_Vader_808 about 6 years
    As mentioned by @JackKalish and others, this breaks the browser "back" button behavior. Not only that, it also breaks the forward button and when you hold either the back or forward button and select a specific history state, it also breaks. I've provided an answer below that seems to work and prevents any of those issues, while answering the original question. Hope it helps.
  • Byron Lopez
    Byron Lopez about 6 years
    The element to be scrolled might not be in the scrollContainer first node, you might need to dig a bit in the object, for me what it really worked was scrollContainer .scrollable._elementRef.nativeElement.scrollTop = 0
  • 1in9ui5t
    1in9ui5t almost 6 years
    This only works for back, it breaks on forward if you carefully test it
  • Matt Thomas
    Matt Thomas almost 6 years
  • Simon Mathewson
    Simon Mathewson almost 6 years
  • Simon Mathewson
    Simon Mathewson almost 6 years
  • BBaysinger
    BBaysinger almost 6 years
    Awesome. I had to make a slightly custom version to scroll a div rather than window, but it worked. One key difference was scrollTop vs scrollY.
  • PiyaModi
    PiyaModi over 5 years
    For me, it's not able to subscribe to the location. My this.lastPoppedUrl is always undefined. Can anyone explain?
  • ZirkoViter
    ZirkoViter over 5 years
    Is there a way to enable "Router Scroll Position Restoration" for nested elements or it works only for body?
  • ryanovas
    ryanovas almost 5 years
    Even though the answer above is more descriptive I like that this answer told me exactly where this needs to go
  • Alan Smith
    Alan Smith almost 5 years
    This doesn't seem to work with modules that use RouterModule.forChild(routes). The forChild function doesn't accept ExtraOptions.
  • Fernando Echeverria
    Fernando Echeverria almost 5 years
    @AlanSmith that's true, but there should always be a call to RouterModuel.forRoot in the main app module, and that's where you should add this option. It's not necessary to add it also in other modules.
  • Alan Smith
    Alan Smith almost 5 years
    Doesn't work when you only add it to the root module, when the loaded page you need to scroll up on is defined in a child (lazy loaded) module with it's own routes.
  • iBlehhz
    iBlehhz almost 5 years
    I added a window.setTimeout - I'm not sure why but for me when I press the browser back button the page does not automatically scroll to the correct position. I've posted my workaround here stackoverflow.com/questions/57214772/…. Let me know if there's other better ways to do it. Thanks!
  • Ravi Naidu
    Ravi Naidu over 4 years
    If you are using Angular v6.1 and above as mentioned in the answer scrollPositionRestoration and anchorScrolling options worked well for me. Explained well in the blog: [medium.com/lacolaco-blog/…
  • F3L1X79
    F3L1X79 over 4 years
    For your information the answer from @Fernando Echeverria is more useful and concise for Angular 6+ than this accepted answer
  • Ruben
    Ruben over 4 years
    see answer below for angular 6.1+: RouterModule.forRoot(routes, {scrollPositionRestoration: 'enabled'})
  • Joey Gough
    Joey Gough about 4 years
    instead of the if block, you might be able to use pipe and filter like this this.router.events.pipe( filter(event => event instanceof NavigationEnd) ).subscribe((e: NavigationEnd ) => { window.scrollTo(0, 0); })
  • dude
    dude over 3 years
    Rather use ViewportScroller as it's officially provided by angular instead of hardcoded window.scrollTop. For example this.viewportScroller.scrollToPosition([0, 0]);
  • AppDreamer
    AppDreamer almost 3 years
    Great! Now I just need to know how to declare "document" and "filter". Cheers!
  • manuel
    manuel over 2 years
    this seams to be the "official" answer. Is it possible to scroll to top only on some links ?
  • Dhritiman Tamuli Saikia
    Dhritiman Tamuli Saikia about 2 years
    You don't need to do all this now. Now, the scrollPositionRestoration is modified to restore the previous page's scroll position(if any) or scroll to top if not when we set the scrollPositionRestoration to 'enabled'
  • Pierre
    Pierre almost 2 years
    This works great when you have inner scrollable divs.
  • Suamere
    Suamere almost 2 years
    Of course you CAN follow the other answers and specify this on each and every controller. But this solution works perfect and applies to all router-outlet changes without having to go to every controller. Beautiful.