In Angular's router, what do the scrollPositionRestoration and anchorScrolling options of withInMemoryScrolling() do, and what happens without them?
answer
- the window keeps its offset by default
- 'top' versus 'enabled'
- positions stored per navigation id
- anchors only on forward navigations
- restore runs before async data arrives
basics
~20 sWithout withInMemoryScrolling() the router never scrolls, so a new route opens at the old offset. scrollPositionRestoration 'top' resets to the top, even on Back; 'enabled' restores the saved position on Back/Forward. anchorScrolling 'enabled' scrolls to the URL fragment's element.
solid answer
~40 sBoth options default to `'disabled'`, and without the feature the router does no scrolling at all, so the window simply keeps its offset and a new page can open half-way down. `scrollPositionRestoration: 'top'` scrolls to `[0, 0]` after navigations, Back included, unless anchor scrolling targets a fragment. `'enabled'` records the window position at each `NavigationStart`; on a Back/Forward (popstate) navigation it restores the stored position, otherwise it goes to the anchor or the top. `anchorScrolling: 'enabled'` scrolls to the element whose id or name matches the fragment, but only on forward navigations. The router emits a `Scroll` event a macrotask or animation frame after `NavigationEnd`, so a list that loads its data asynchronously is often still short when the restore runs, and the user lands near the top.
code
ts · 42 linesimport {Component, Injector, afterNextRender, inject, signal} from '@angular/core';
import {ViewportScroller} from '@angular/common';
import {Router, Scroll} from '@angular/router';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {filter, take} from 'rxjs';
interface Hit {
id: number;
title: string;
}
@Component({
selector: 'app-results',
template: `@for (hit of hits(); track hit.id) { <p>{{ hit.title }}</p> }`,
})
export class Results {
private readonly scroller = inject(ViewportScroller);
private readonly injector = inject(Injector);
readonly hits = signal<Hit[]>([]);
private readonly loaded = fetch('/api/hits')
.then((res) => res.json() as Promise<Hit[]>)
.then((hits) => this.hits.set(hits));
constructor() {
inject(Router)
.events.pipe(
filter((e): e is Scroll => e instanceof Scroll),
take(1),
takeUntilDestroyed(),
)
.subscribe((e) => {
const position = e.position;
if (!position) return;
this.loaded.then(() =>
afterNextRender(() => this.scroller.scrollToPosition(position), {
injector: this.injector,
}),
);
});
}
}go deeper
Remember that the router does not scroll by default, and that withInMemoryScrolling with 'top' or 'enabled' plus anchorScrolling fixes the half-way-down page.
Explain 'top' versus 'enabled': positions are stored at NavigationStart and restored only on Back/Forward, while anchors apply only to forward navigations.
Diagnose the Back-lands-at-top bug: the Scroll event fires after first render but before async data, so re-scroll after load, keep the view alive, or preload the data.
Decide on one scroll policy for the whole app, including inner scroll containers and sticky headers, because per-page fixes drift and users feel every inconsistency.
## The default: the router does not scroll In a single-page app the document is never reloaded, so the browser has no reason to move the window when the route changes. Unless you add `withInMemoryScrolling()` to `provideRouter`, Angular's router creates no scroller at all. The result is the classic complaint: a user scrolls a long page, clicks a link, and the next route renders **at the same vertical offset**. Both options of the feature also default to `'disabled'`, so `withInMemoryScrolling()` with no arguments changes nothing visible. ## The two options | Option | Value | Behaviour | |---|---|---| | `scrollPositionRestoration` | `'disabled'` (default) | no scrolling; the window keeps its offset | | | `'top'` | scroll to `[0, 0]` after navigations, including Back/Forward; a forward navigation with a fragment goes to the anchor if anchor scrolling is on | | | `'enabled'` | Back/Forward restores the stored position; other navigations go to the anchor (if anchor scrolling is on) or to the top | | `anchorScrolling` | `'disabled'` (default) | the URL fragment is ignored for scrolling | | | `'enabled'` | on a forward navigation with a fragment, scroll to the element whose `id` (or `name`) matches it | A few details that interviewers like to probe: - Anchor scrolling **does not run on popstate**. Going Back to `/docs#install` restores the stored position instead of jumping to the anchor. - Clicking a link to the **same URL with a fragment** still scrolls to the anchor, even though the router skips the navigation itself. - When restoration is anything other than `'disabled'`, the router sets `history.scrollRestoration` to `'manual'` so the browser and the router do not both try to scroll. - The router scrolls the **window** through `ViewportScroller`. A scrollable inner container (a `div` with `overflow: auto`) is not saved or restored. - A sticky header that hides anchored headings is handled by injecting `ViewportScroller` and calling `setOffset()`; in NgModule apps `RouterModule.forRoot()` also accepts a `scrollOffset` option. ## How the restore is timed 1. On `NavigationStart` the router stores the current window position under the id of the navigation that is being left. 2. On `NavigationEnd` it schedules a **`Scroll` router event**. The event is deliberately delayed until the next macrotask or animation frame, whichever comes first, so the newly activated component has rendered. 3. The scroller consumes the `Scroll` event: if it carries a stored `position` (Back/Forward) it scrolls there; otherwise it uses the anchor or `[0, 0]` according to the options. ## The async-data trap The delay covers the first render, not your HTTP call. A results page that fetches its list when it is created is still empty, or showing a skeleton, when the restore fires. The browser clamps the requested offset to the short page and the user lands near the top. Three standard fixes: - **Scroll again once the data is there.** Subscribe to `Router.events`, keep the `Scroll` event's `position`, and after the list renders call `ViewportScroller.scrollToPosition(position)`. This is the approach the router's own documentation sketches. - **Keep the list alive.** A custom `RouteReuseStrategy` detaches the results view instead of destroying it; on Back the whole DOM is reinserted at once and the stored position is valid immediately. - **Have the data before activation**, for example through a resolver or a cache, so the first render is already full height. ## Choosing a setting - **Content sites and documentation** usually want `'enabled'` plus `anchorScrolling: 'enabled'`: new pages start at the top, Back returns the reader to where they were, and heading links work. - **Dashboards with a fixed shell** where only an inner panel scrolls get little from the feature, because the window never scrolls; they need their own container handling. - **Apps that must control scrolling per navigation** can still enable the feature and handle the `Scroll` event themselves where the default is wrong, for example re-scrolling after data loads. Current versions also let an individual navigation opt out of the router's scrolling through its navigation options. Whatever you choose, test it with the real data latency of production, because a restore that works against a local mock can fail against a slow API. ## Configuring it ```ts provideRouter( routes, withInMemoryScrolling({scrollPositionRestoration: 'enabled', anchorScrolling: 'enabled'}), ); ``` The documentation for `scrollPositionRestoration` notes that `'enabled'` is intended to become the default in the future; today it is opt-in. In an NgModule app the same options go straight into `RouterModule.forRoot(routes, {...})`.
- With anchorScrolling enabled, why does pressing Back to /docs#install not jump to the install heading?The router treats Back/Forward as a traversal: it restores the window position it stored when the user left that entry, and anchor scrolling is skipped for popstate navigations. Anchor scrolling only runs on forward navigations, including a click on a link to the same URL with a fragment.
- Your app scrolls a main content div instead of the window. Why does scrollPositionRestoration 'enabled' do nothing, and what do you do?The router saves and restores the window offset through `ViewportScroller`, which reads and writes `window` scroll positions. An inner container's `scrollTop` is never recorded. Either let the document scroll, or save the container's offset yourself, for example keyed by navigation id from `NavigationStart`, and restore it after the `Scroll` event.
saying these in an interview costs you the question
- The router scrolls to the top by default
- 'enabled' restores position on every navigation, forward included
- Anchor scrolling also runs when pressing Back
- withInMemoryScrolling() with no options turns restoration on
- Restoration waits until HTTP data has loaded
- The router restores scroll inside any overflow container