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
| Option | Type | Default | Description |
|---|---|---|---|
target | MaybeElementSignal<Element> | Window | window | Element or window to track scroll on |
throttle | number | 0 | Throttle scroll events (ms) |
offset | { top?, bottom?, left?, right? } | {} | Offset for arrived detection |
injector | Injector | - | Optional injector for DI context |
Return Value
The scrollPosition() function returns a ScrollPositionRef:
| Property | Type | Description |
|---|---|---|
x | Signal<number> | Horizontal scroll position |
y | Signal<number> | Vertical scroll position |
isScrolling | Signal<boolean> | Whether currently scrolling |
arrivedState | Signal<ArrivedState> | Which edges have been reached |
directions | Signal<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
Sticky header
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:
x→0y→0isScrolling→falsearrivedState→{ 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;Related
- MousePosition — Track mouse position
- WindowSize — Track window dimensions