IntersectionObserver

Low-level utility for observing element intersection with viewport using the IntersectionObserver API. Provides fine-grained control over observation lifecycle.

Loading demo...

Usage

Single element observation

Observe a single element by passing it as the first parameter:

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

@Component({ 
  template: `<div>Scroll to see intersection!</div>`,
})
export class Intersection {
  readonly el = inject(ElementRef);
  readonly observer = intersectionObserver(this.el, console.log); 
}

Multiple elements observation

Observe multiple elements by passing an array:

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

@Component({
  template: `
    <div #child1>Child 1</div>
    <div #child2>Child 2</div>
  `,
})
export class MultipleIntersection {
  readonly child1 = viewChild<ElementRef>('child1');
  readonly child2 = viewChild<ElementRef>('child2');
  
  readonly observer = intersectionObserver(
    [this.child1, this.child2], 
    (entries, observer) => {
      for (const entry of entries) {
        console.log('Intersecting:', entry.isIntersecting);
      }
    }
  );
}

With reactive options

All options (root, rootMargin, threshold) can be reactive:

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

@Component({
  template: `
    <div #container>
      <div #box>Observed element</div>
    </div>
    <button (click)="threshold.set(threshold() === 0.5 ? 0.9 : 0.5)">
      Toggle threshold
    </button>
  `,
})
export class ReactiveOptions {
  readonly box = viewChild<ElementRef>('box');

  readonly root = viewChild<ElementRef>('container'); 
  readonly rootMargin = input('10px'); 
  readonly threshold = signal(0.5); 
  
  readonly observer = intersectionObserver(
    this.box,
    entries => { /* callback */ },
    {
      root: this.root, // Reactive root element
      rootMargin: this.rootMargin, // Reactive margin
      threshold: this.threshold, // Reactive threshold
    }
  );
}

Manual cleanup

Observers are automatically disconnected after the view is destroyed (see Automatic cleanup). However, the returned IntersectionObserverRef can be destroyed to stop observation manually:

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

@Component({
  template: `<div #box>Scroll me!</div>`,
})
export class ManualCleanup {
  readonly box = viewChild<ElementRef>('box');
  readonly observer = intersectionObserver(this.box, console.log);

  manualCleanup() {
    // Stop observing
    this.observer.destroy();
  }
}

Parameters

ParameterTypeDescription
targetMaybeElementSignal<Element> | MaybeElementSignal<Element>[]Element(s) to observe. Can be a single element or array. Can be:
- A plain Element or ElementRef<Element>
- A Signal<Element> or Signal<ElementRef<Element>>
- undefined (observation is skipped)
- An array of any of the above
callback(entries: readonly IntersectionObserverEntry[], observer: IntersectionObserver) => voidCallback function called when observed elements intersect with viewport
optionsIntersectionObserverInitOptionsOptional configuration (see Options below)

Options

The IntersectionObserverInitOptions extends Omit<CreateEffectOptions, 'allowSignalWrites'>:

OptionTypeDefaultDescription
rootMaybeElementSignal<Element> | Document | null-The element that is used as the viewport for checking visibility
rootMarginMaybeSignal<string>-Margin around the root element
thresholdMaybeSignal<number | number[]>-Threshold(s) for intersection
manualCleanupbooleanfalseIf true, the effect requires manual cleanup. By default, the effect automatically registers itself for cleanup with the current DestroyRef
debugNamestring-Debug name for the effect (used in Angular DevTools)
injectorInjector-Optional injector for DI context

Return Value

Returns an IntersectionObserverRef with a destroy() method to stop observing the element(s).

Examples

Scroll animations

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

@Component({
  template: `
    <div #section [class.visible]="isVisible()">
      Content that animates on scroll
    </div>
  `,
})
export class ScrollAnimation {
  readonly section = viewChild<ElementRef>('section');
  readonly isVisible = signal(false);

  constructor() {
    intersectionObserver(
      this.section,
      entries => {
        for (const entry of entries) {
          this.isVisible.set(entry.isIntersecting);
        }
      },
      { threshold: 0.1 }
    );
  }
}

Conditional observation

Observe elements conditionally:

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

@Component({
  template: `
    <div #box>Box</div>
    <button (click)="enabled.set(!enabled())">
      {{ enabled() ? 'Disable' : 'Enable' }} Observation
    </button>
  `,
})
export class ConditionalIntersection {
  readonly box = viewChild<ElementRef>('box');
  readonly enabled = signal(true);
  
  readonly observer = intersectionObserver(
    computed(() => (this.enabled() ? this.box() : undefined)), 
    (entries, observer) => {
      console.log('Intersected:', entries);
    }
  );
}

SSR Compatibility

On the server, intersectionObserver returns a no-op IntersectionObserverRef that safely handles destroy() calls without creating actual observers.

Type Definitions

typescript
interface IntersectionObserverInitOptions extends Omit<CreateEffectOptions, 'allowSignalWrites'> {
  readonly root?: MaybeElementSignal<Element> | Document | null;
  readonly rootMargin?: MaybeSignal<string>;
  readonly threshold?: MaybeSignal<number | number[]>;
}

interface IntersectionObserverRef {
  readonly destroy: () => void;
}

function intersectionObserver(
  target: MaybeElementSignal<Element> | MaybeElementSignal<Element>[],
  callback: (entries: readonly IntersectionObserverEntry[], observer: IntersectionObserver) => void,
  options?: IntersectionObserverInitOptions
): IntersectionObserverRef;
Edit this page on GitHub Last updated: Mar 19, 2026, 23:28:23