Core

Viewport Scroller

The Viewport Scroller replaces Angular's built-in ViewportScroller. Anchor scrolling honors the CSS scroll margin of the anchor, and scroll restoration also works in layouts where an element scrolls instead of the page.

Overview

The router scrolls to anchors and restores scroll positions through Angular's ViewportScroller. The built-in implementation ignores the scroll-margin of the anchor, so headings end up hidden behind sticky headers, and it only ever scrolls the window.

CSS Offsets
Anchors are scrolled into view natively, so the offset is the scroll-margin of the anchor element.
Element Scrolling
Layouts that scroll a panel instead of the page can register that panel with the buiScrollViewport directive.
SSR Safe
Every method is inert on the server, where the WINDOW token is null.

Provider Setup

Add provideViewportScroller to the providers array of your application configuration and enable the router's withInMemoryScrolling feature for anchor scrolling and scroll position restoration. It requires provideWindow:

import { provideViewportScroller } from '@/app/core/viewport-scroller';

export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withInMemoryScrolling({
anchorScrolling: 'enabled',
scrollPositionRestoration: 'enabled',
})
),
provideViewportScroller(),
provideWindow(),
// ...other providers
],
};

Anchor Offsets

The offset of an anchor is its CSS scroll-margin. Use the Tailwind scroll-mt-* utilities to keep a heading clear of a sticky header:

<h2 id="installation" class="scroll-mt-20">Installation</h2>

To give every anchor a default offset, set it once in your global styles. The rule lives in the base layer and Tailwind utilities come after it, so any scroll-mt-* class on an anchor overrides it:

@layer base {
:where([id]) {
scroll-margin-top: 4rem;
}
}

The setOffset method of the ViewportScroller has no effect. Offsets always come from CSS.

Scrolling Elements

By default the window scrolls. When your layout scrolls an element instead, for example a content panel with overflow-y-auto, add the buiScrollViewport directive to that element. Scroll restoration and anchor scrolling then target it:

@Component({
selector: 'app-layout',
imports: [BuiScrollViewport, RouterOutlet],
template: `
<div class="flex h-dvh">
<nav class="w-64 shrink-0">...</nav>
<main
buiScrollViewport
class="flex-1 overflow-y-auto"
>
<router-outlet />
</main>
</div>
`,
})
export class Layout {}

Only one element can be registered at a time. It is cleared when the directive is destroyed, and the window takes over again.

Programmatic Scrolling

Injecting Angular's ViewportScroller resolves to this implementation, so application code can scroll the same way the router does:

private viewportScroller = inject(ViewportScroller);

scrollToTop() {
this.viewportScroller.scrollToPosition([0, 0], { behavior: 'smooth' });
}

scrollToPricing() {
this.viewportScroller.scrollToAnchor('pricing', { behavior: 'smooth' });
}