ElementVisibility
Reactive tracking of element visibility in the viewport using IntersectionObserver. Perfect for lazy loading and scroll animations.
Loading demo...
Usage
angular-ts
import { Component, viewChild, ElementRef } from '@angular/core';
import { elementVisibility } from '@signality/core';
@Component({
template: `
<div #section [class.visible]="visibility().isVisible">
{{ visibility().isVisible ? 'In viewport!' : 'Scroll to see me' }}
</div>
`,
})
export class VisibilityDemo {
readonly section = viewChild<ElementRef>('section');
readonly visibility = elementVisibility(this.section);
}Parameters
| Parameter | Type | Description |
|---|---|---|
target | MaybeElementSignal<HTMLElement> | Target element to observe |
options | ElementVisibilityOptions | Optional configuration (see Options below) |
Options
The ElementVisibilityOptions extends CreateSignalOptions<ElementVisibilityValue> and WithInjector:
| Option | Type | Default | Description |
|---|---|---|---|
threshold | MaybeSignal<number | number[]> | 0 | Visibility threshold(s) |
root | MaybeElementSignal<Element> | Document | undefined | Scrollable ancestor (null = viewport) |
rootMargin | MaybeSignal<string> | '0px' | Margin around root |
initialValue | ElementVisibilityValue | { isVisible: true, ratio: 1 } | Initial value for SSR |
equal | ValueEqualityFn<ElementVisibilityValue> | - | Custom equality function (see more) |
debugName | string | - | Debug name for the signal (development only) |
injector | Injector | - | Optional injector for DI context |
Return Value
The elementVisibility() function returns a Signal<ElementVisibilityValue>:
typescript
interface ElementVisibilityValue {
isVisible: boolean;
ratio: number;
}| Property | Type | Description |
|---|---|---|
isVisible | boolean | Whether element is visible in viewport |
ratio | number | Intersection ratio (0.0 to 1.0) |
Examples
Scroll animation
angular-ts
import { Component, viewChild, ElementRef, computed } from '@angular/core';
import { elementVisibility } from '@signality/core';
@Component({
template: `
<section
#section
class="fade-section"
[style.opacity]="opacity()"
[style.transform]="transform()"
>
<h2>Animated Section</h2>
<p>This fades in as you scroll</p>
</section>
`,
})
export class ScrollFadeIn {
readonly section = viewChild<ElementRef>('section');
readonly visibility = elementVisibility(this.section, {
threshold: [0, 0.25, 0.5, 0.75, 1]
});
readonly opacity = computed(() => this.visibility().ratio);
readonly transform = computed(() => {
const ratio = this.visibility().ratio;
const translateY = (1 - ratio) * 30;
return `translateY(${translateY}px)`;
});
}Infinite scroll trigger
angular-ts
import { Component, viewChild, ElementRef, effect, signal } from '@angular/core';
import { elementVisibility } from '@signality/core';
@Component({
template: `
<div class="list">
@for (item of items(); track item.id) {
<div class="item">{{ item.name }}</div>
}
</div>
<div #trigger class="load-trigger">
@if (loading()) {
<spinner />
}
</div>
`,
})
export class InfiniteScroll {
readonly trigger = viewChild<ElementRef>('trigger');
readonly visibility = elementVisibility(this.trigger, {
rootMargin: '100px' // Load before reaching bottom
});
readonly items = signal<any[]>([]);
readonly loading = signal(false);
constructor() {
effect(() => {
if (this.visibility().isVisible && !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);
}
}Read progress tracking
angular-ts
import { Component, viewChild, ElementRef, computed } from '@angular/core';
import { elementVisibility } from '@signality/core';
@Component({
template: `
<article #article>
<h1>Long Article</h1>
<p>Content...</p>
</article>
<div class="progress-bar">
<div [style.width.%]="readProgress()"></div>
</div>
`,
})
export class ReadProgress {
readonly article = viewChild<ElementRef>('article');
readonly visibility = elementVisibility(this.article, {
threshold: Array.from({ length: 101 }, (_, i) => i / 100)
});
readonly readProgress = computed(() =>
Math.round(this.visibility().ratio * 100)
);
}Scrollable container with reactive root
angular-ts
import { Component, signal, viewChild, ElementRef } from '@angular/core';
import { elementVisibility } from '@signality/core';
@Component({
template: `
<div #container class="scroll-container">
<div #section [class.visible]="visibility().isVisible">
Section inside scrollable container
</div>
</div>
`,
})
export class ScrollableContainer {
readonly container = viewChild<ElementRef>('container');
readonly section = viewChild<ElementRef>('section');
// Root element updates reactively when container becomes available
readonly visibility = elementVisibility(this.section, {
root: this.container,
rootMargin: '50px',
});
}SSR Compatibility
On the server, signals initialize with the initialValue (or safe defaults):
isVisible→true(or custominitialValue.isVisible)ratio→1(or custominitialValue.ratio)
Type Definitions
typescript
interface ElementVisibilityValue {
readonly isVisible: boolean;
readonly ratio: number;
}
interface ElementVisibilityOptions
extends CreateSignalOptions<ElementVisibilityValue>,
WithInjector {
readonly threshold?: MaybeSignal<number | number[]>;
readonly root?: MaybeElementSignal<Element> | Document;
readonly rootMargin?: MaybeSignal<string>;
readonly initialValue?: ElementVisibilityValue;
}
function elementVisibility(
target: MaybeElementSignal<HTMLElement>,
options?: ElementVisibilityOptions
): Signal<ElementVisibilityValue>;