Equivalent to autoscroll=true: automatic scroll to top when route changes

When navigating within our SPA, we want to scroll back to the top of the page whenever the route changes. Users have commented that it is really odd if clicking a link at the foot of the page causes the content to change but leaves you still at the bottom.

I’m not sure how best to handle this scenario, and would like to avoid having to add code to every top level component. This feels like the kind of thing that should be part of Angular itself, like autoscroll=true in Angular 1.

10 thoughts on “Equivalent to autoscroll=true: automatic scroll to top when route changes

  1. For router 3.0.0-beta.2 this works:

    this.router.events.subscribe((path) => {
          if (path.url != this.url) {
            window.scrollTo(0, 0);
          }
     });
  2. What if I’m navigating back? I can’t scroll to top, I must recover the previous scroll. How to deal with it? Imagine the scenario:

    Home has a long list of products, you scroll down to the end and click on some product. Now you navigate back, but that long products list is not populated yet because it is an async request, so that the browser can’t recover that scroll position.

  3. Late to the party, but here’s my two cents…

    Edit: I should have mentioned that this answers the question of how to reset the scroll bar back to the top when changing routes. Also, it ensures that the browser’s back and forward buttons aren’t broken. Meaning, when you click back or forward, the scroll bar is set back to the position when you had initially left the page. Also, there’s an edge case where if you click and hold the back or forward browser buttons everything works, unlike the other answers I’ve seen so far. Really hope this helps…

    export class AppComponent implements OnInit {
      isPopState = false;
      
      constructor(private router: Router, private locStrat: LocationStrategy) { }
      
      ngOnInit(): void {
    	this.locStrat.onPopState(() => {
    	  this.isPopState = true;
    	});
    	
    	this.router.events.subscribe(event => {
    	  // Scroll to top if accessing a page, not via browser history stack
    	  if (event instanceof NavigationEnd && !this.isPopState) {
    		window.scrollTo(0, 0);
    		this.isPopState = false;
    	  }
    	  
    	  // Ensures that isPopState is reset
    	  if (event instanceof NavigationEnd) {
    		this.isPopState = false;
    	  }
    	});
      }
    }
    
  4. This worked for me in Angular 6 using Angular Material 6:

    // app.component.html
    <mat-sidenav-container class="sidenav-container">
            <mat-sidenav #drawer class="sidenav"
            [fixedInViewport]="(isHandset | async)!.matches"
            [mode]="(isHandset | async)!.matches ? 'over' : 'side'"
            [opened]="!(isHandset | async)!.matches">
                <mat-toolbar color="primary">Menu</mat-toolbar>
                <mat-nav-list>
                    <a mat-list-item routerLink="/">Home</a>
                    . . . . 
                </mat-nav-list>
            </mat-sidenav>
            <mat-sidenav-content>
                <nav>
                    <mat-toolbar color="primary" class="mat-elevation-z6">
                        <button type="button" aria-label="Toggle sidenav" mat-icon-button (click)="drawer.toggle()" *ngIf="(isHandset | async)!.matches">
                            <mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
                        </button>
                        <span>Ng6FireBlog</span>
                    </mat-toolbar>
                </nav>
                <section class="content" >
                    <router-outlet ></router-outlet>
                </section>
            </mat-sidenav-content>
        </mat-sidenav-container>
    
    // app.component.ts
    import { Component, OnInit } from '@angular/core';
    import { Router, NavigationEnd } from '@angular/router';
    import { BreakpointObserver, Breakpoints, BreakpointState } from '@angular/cdk/layout';
    import { Observable } from 'rxjs';
    
    @Component({
        selector: 'app-root',
        templateUrl: './app.component.html',
        styleUrls: ['./app.component.css']
    })
    export class AppComponent implements OnInit {
        window: Element;
        isHandset: Observable<BreakpointState> = this.breakpointObserver.observe(Breakpoints.Handset);
    
        constructor(private router: Router,private breakpointObserver: BreakpointObserver) { }
    
        ngOnInit(): void {
            this.router.events.subscribe(event => {
                // Scroll to top if accessing a page, not via browser history stack
                if (event instanceof NavigationEnd) {
                    const contentContainer = document.querySelector('.mat-sidenav-content') || this.window;
                    contentContainer.scrollTo(0, 0);
                }
            });
        }
    }
    
    // app.component.css
    .sidenav-container {
        height: 100vh;
        width: 100vw;
        -webkit-box-flex: 1;
        -ms-flex: 1;
        flex: 1
    }
    
    .sidenav {
        width: 200px;
        box-shadow: 3px 0 6px rgba(0, 0, 0, .24);
    }
    
    nav mat-toolbar {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        z-index: 1
    }