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

ParameterTypeDescription
targetMaybeElementSignal<HTMLElement>Target element to observe
optionsElementVisibilityOptionsOptional configuration (see Options below)

Options

The ElementVisibilityOptions extends CreateSignalOptions<ElementVisibilityValue> and WithInjector:

OptionTypeDefaultDescription
thresholdMaybeSignal<number | number[]>0Visibility threshold(s)
rootMaybeElementSignal<Element> | DocumentundefinedScrollable ancestor (null = viewport)
rootMarginMaybeSignal<string>'0px'Margin around root
initialValueElementVisibilityValue{ isVisible: true, ratio: 1 }Initial value for SSR
equalValueEqualityFn<ElementVisibilityValue>-Custom equality function (see more)
debugNamestring-Debug name for the signal (development only)
injectorInjector-Optional injector for DI context

Return Value

The elementVisibility() function returns a Signal<ElementVisibilityValue>:

typescript
interface ElementVisibilityValue {
  isVisible: boolean;
  ratio: number;
}
PropertyTypeDescription
isVisiblebooleanWhether element is visible in viewport
rationumberIntersection 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):

  • isVisibletrue (or custom initialValue.isVisible)
  • ratio1 (or custom initialValue.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>;
Edit this page on GitHub Last updated: Mar 19, 2026, 23:28:23