ScrollPosition

Reactive tracking of scroll position. Track scroll offset of window or any scrollable element.

Loading demo...

Usage

angular-ts
import { Component } from '@angular/core';
import { scrollPosition } from '@signality/core';

@Component({
  template: `
    <p>Scroll Y: {{ scrollPos.y() }}px</p>
  `,
})
export class ScrollDemo {
  readonly scrollPos = scrollPosition(); 
}

Options

OptionTypeDefaultDescription
targetMaybeElementSignal<Element> | WindowwindowElement or window to track scroll on
throttlenumber0Throttle scroll events (ms)
offset{ top?, bottom?, left?, right? }{}Offset for arrived detection
injectorInjector-Optional injector for DI context

Return Value

The scrollPosition() function returns a ScrollPositionRef:

PropertyTypeDescription
xSignal<number>Horizontal scroll position
ySignal<number>Vertical scroll position
isScrollingSignal<boolean>Whether currently scrolling
arrivedStateSignal<ArrivedState>Which edges have been reached
directionsSignal<ScrollDirections>Current scroll direction
typescript
interface ArrivedState {
  top: boolean;
  bottom: boolean;
  left: boolean;
  right: boolean;
}

interface ScrollDirections {
  top: boolean;
  bottom: boolean;
  left: boolean;
  right: boolean;
}

Examples

angular-ts
import { Component, computed } from '@angular/core';
import { scrollPosition } from '@signality/core';

@Component({
  template: `
    <header [class.sticky]="isSticky()">
      <h1>My App</h1>
    </header>
    <main>Content...</main>
  `,
  styles: `
    header.sticky {
      position: fixed;
      top: 0;
      background: white;
      box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    }
  `,
})
export class StickyHeader {
  readonly scrollPos = scrollPosition();
  readonly isSticky = computed(() => this.scrollPos.y() > 100);
}

Back to top button

angular-ts
import { Component, computed } from '@angular/core';
import { scrollPosition } from '@signality/core';

@Component({
  template: `
    @if (showButton()) {
      <button class="back-to-top" (click)="scrollToTop()">
        ↑ Top
      </button>
    }
  `,
})
export class BackToTop {
  readonly scrollPos = scrollPosition();
  readonly showButton = computed(() => this.scrollPos.y() > 500);

  scrollToTop() {
    window.scrollTo({ top: 0, behavior: 'smooth' });
  }
}

Scroll progress bar

angular-ts
import { Component, computed } from '@angular/core';
import { scrollPosition } from '@signality/core';

@Component({
  template: `
    <div class="progress-bar">
      <div class="progress" [style.width.%]="progress()"></div>
    </div>
  `,
})
export class ScrollProgress {
  readonly scrollPos = scrollPosition();

  readonly progress = computed(() => {
    if (!window) return 0;
    const docEl = document.documentElement;
    const scrollHeight = docEl.scrollHeight - window.innerHeight;
    if (scrollHeight <= 0) return 0;
    return (this.scrollPos.y() / scrollHeight) * 100;
  });
}

Infinite scroll

angular-ts
import { Component, effect, signal } from '@angular/core';
import { scrollPosition } from '@signality/core';

@Component({
  template: `
    <div class="list">
      @for (item of items(); track item.id) {
        <div class="item">{{ item.name }}</div>
      }
    </div>
    @if (loading()) {
      <div class="loading">Loading...</div>
    }
  `,
})
export class InfiniteList {
  readonly scrollPos = scrollPosition({ offset: { bottom: 200 } });
  readonly items = signal<any[]>([]);
  readonly loading = signal(false);

  constructor() {
    effect(() => {
      if (this.scrollPos.arrivedState().bottom && !this.loading()) {
        this.loadMore(); 
      }
    });
  }

  async loadMore() {
    this.loading.set(true);
    const newItems = await this.fetchItems();
    this.items.update(items => [...items, ...newItems]);
    this.loading.set(false);
  }
}

Scrollable container

angular-ts
import { Component, viewChild, ElementRef } from '@angular/core';
import { scrollPosition } from '@signality/core';

@Component({
  template: `
    <div #container class="scroll-container">
      <div class="content">Long content...</div>
    </div>
    <div class="scroll-indicator">
      @if (!scrollPos.arrivedState().bottom) {
        <span>↓ Scroll for more</span>
      }
    </div>
  `,
})
export class ScrollContainer {
  readonly container = viewChild<ElementRef>('container');
  readonly scrollPos = scrollPosition({ target: this.container }); 
}

SSR Compatibility

On the server, signals initialize with safe defaults:

  • x0
  • y0
  • isScrollingfalse
  • arrivedState{ top: true, bottom: false, left: true, right: false }
  • directions{ top: false, bottom: false, left: false, right: false }

Type Definitions

typescript
interface ArrivedState {
  readonly top: boolean;
  readonly bottom: boolean;
  readonly left: boolean;
  readonly right: boolean;
}

interface ScrollDirections {
  readonly top: boolean;
  readonly bottom: boolean;
  readonly left: boolean;
  readonly right: boolean;
}

interface ScrollPositionOptions extends WithInjector {
  readonly target?: MaybeElementSignal<Element> | Window;
  readonly throttle?: number;
  readonly offset?: {
    readonly top?: number;
    readonly bottom?: number;
    readonly left?: number;
    readonly right?: number;
  };
}

interface ScrollPositionRef {
  readonly x: Signal<number>;
  readonly y: Signal<number>;
  readonly isScrolling: Signal<boolean>;
  readonly arrivedState: Signal<ArrivedState>;
  readonly directions: Signal<ScrollDirections>;
}

function scrollPosition(options?: ScrollPositionOptions): ScrollPositionRef;
Edit this page on GitHub Last updated: Mar 19, 2026, 23:28:23