Vize

intersection-observer

Observe the intersection of reactive elements with a root.

Package @vizejs/composable/intersection-observer
Own the source vize lib pull composable:intersection-observer
Runtime exports useIntersectionObserver, useElementVisibility
Gzip budget 2816 B

Minimal setup

Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).

vp install @vizejs/composable

Import the entry directly. Call lifecycle-bound utilities inside <script setup> so cleanup follows the component scope; consult the runtime contract for this entry's SSR and hydration behavior.

import { useIntersectionObserver, useElementVisibility } from "@vizejs/composable/intersection-observer";

Try it

Measure whether a featured card crosses a real 60 percent visibility threshold and expose pause, resume, and permanent stop.

Open the interactive example

What to observe: Show featured guide scrolls the actual bounded viewport to the card. Once at least 60 percent is visible, the readout and card highlight change. Pause, return to preparation, then Resume: the resumed observer measures the current hidden position. Stop retains the last recorded visibility while the native scrolling controls continue working.

The template refs supply a real ancestor root and article target to the browser's IntersectionObserver. Native entries supply intersectionRatio and isIntersecting; the example applies the 0.6 threshold to its highlight. isActive describes requested observation, not a guarantee that an observer is connected. The callback explicitly ignores entries while inactive so queued delivery does not overwrite this example's paused readout. Pause disconnects and permits resume; stop permanently disables reconnecting. During SSR support is false and no geometry is read. Mounted targets resolve on the client without replacing the initial SSR markup or elements. Scope disposal disconnects the observer. No synthetic observer host is used.

Compare the initial and interacted states
Observe a featured guide inside its own viewport: initial state
Initial state
Observe a featured guide inside its own viewport: state after the verified interactions
After the verified interactions

Copy the example

Save this complete SFC in your Vue project. Its public imports and source are the same ones executed in the preview.

<script setup lang="ts">
import { ref, useTemplateRef } from "vue";
import { useIntersectionObserver } from "@vizejs/composable/intersection-observer";

const viewport = useTemplateRef<HTMLElement>("viewport");
const featured = useTemplateRef<HTMLElement>("featured");
const ratio = ref(0);
const visible = ref(false);
const stopped = ref(false);
const { isSupported, isActive, pause, resume, stop } = useIntersectionObserver(
  featured,
  (entries) => {
    if (!isActive.value) return;
    const entry = entries.at(-1);
    if (!entry) return;
    ratio.value = entry.intersectionRatio;
    visible.value = entry.isIntersecting && entry.intersectionRatio >= 0.6;
  },
  { root: viewport, threshold: 0.6 },
);

function showFeatured(): void {
  if (!viewport.value || !featured.value) return;
  viewport.value.scrollTo({ top: featured.value.offsetTop - 12, behavior: "auto" });
}

function stopObservation(): void {
  stop();
  stopped.value = true;
}
</script>

<template>
  <div class="composable-example">
    <section ref="viewport" class="guide-viewport" aria-label="Scrollable guide cards">
      <div class="guide-space">
        <strong>Preparation</strong>
        <p>Move to the featured guide below.</p>
      </div>
      <article ref="featured" class="featured-guide" :class="{ visible }">
        <strong>Featured guide: keyboard interaction</strong>
        <p>Use native controls before adding shortcuts.</p>
      </article>
      <div class="guide-space">
        <strong>Next steps</strong>
        <p>Review the deployed result.</p>
      </div>
    </section>
    <div class="example-actions">
      <button type="button" @click="showFeatured">Show featured guide</button>
      <button type="button" @click="() => viewport?.scrollTo({ top: 0 })">
        Return to preparation
      </button>
    </div>
    <output aria-live="polite">
      Last observed featured visibility: {{ visible ? "at least 60%" : "below 60%" }} · Visible
      ratio: {{ Math.round(ratio * 100) }}%
    </output>
    <div class="example-actions">
      <button type="button" :disabled="!isActive || stopped" @click="() => pause()">
        Pause visibility observation
      </button>
      <button type="button" :disabled="isActive || stopped" @click="() => resume()">
        Resume visibility observation
      </button>
      <button type="button" :disabled="stopped" @click="stopObservation">
        Stop visibility observation
      </button>
    </div>
    <p>Observation: {{ stopped ? "stopped" : isActive ? "requested" : "paused" }}.</p>
    <p>
      The native observer measures this viewport. The example records entries only while observation
      is active, retaining its last readout while paused. Resume observes the current position; Stop
      permanently disables observation. The native scrolling buttons remain usable.
    </p>
    <p v-if="!isSupported">Intersection observation is not available in this context.</p>
  </div>
</template>

<style scoped>
.guide-viewport {
  position: relative;
  block-size: 160px;
  overflow: auto;
  border: 1px solid currentColor;
  border-radius: 8px;
}
.guide-space {
  box-sizing: border-box;
  block-size: 190px;
  padding: 12px;
}
.featured-guide {
  box-sizing: border-box;
  min-block-size: 90px;
  padding: 12px;
  border: 2px solid currentColor;
}
.featured-guide.visible {
  background: #e8f5e9;
}
</style>

Runtime contract

Utility Category Stability SSR Hydration Cleanup Targets Host globals Uses
useIntersectionObserver dom experimental deterministic-fallback stable caller, reactive-scope web, desktop globalThis, IntersectionObserver resolveElement, resolveElements, tryOnScopeDispose
useElementVisibility dom experimental deterministic-fallback stable caller, reactive-scope web, desktop globalThis, IntersectionObserver useIntersectionObserver

API

useIntersectionObserver

Observe the intersection of reactive elements with a root. One observer is created per combination of resolved targets, root, margin, and threshold; changing any of them disconnects the previous observer first. During server rendering isSupported is false and the callback never runs. The observer is disconnected when the owning scope stops.

function useIntersectionObserver( targets: MaybeElementTargets, callback: IntersectionObserverCallback, options: UseIntersectionObserverOptions = {}, ): IntersectionObserverControls

useElementVisibility

Track whether an element is visible inside the viewport (or a root). Uses the latest intersection entry, so rapid enter/leave sequences settle on the final state. With once, the observer disconnects after the first visible entry and isVisible stays true.

function useElementVisibility( target: MaybeElementTarget, options: UseElementVisibilityOptions = {}, ): ElementVisibilityControls

Types

IntersectionObserverHost

Runtime capability that supplies the IntersectionObserver constructor.

Member Type Description
IntersectionObserver? new ( callback: IntersectionObserverCallback, options?: IntersectionObserverInit, ) => IntersectionObserver IntersectionObserver constructor, absent in unsupported runtimes.

UseIntersectionObserverOptions

Options for useIntersectionObserver.

Member Type Description
root? IntersectionRoot Ancestor (or document) whose box is used as the viewport.
rootMargin? MaybeRefOrGetter<string> Margin grown or shrunk around the root box, in CSS margin syntax.
threshold? MaybeRefOrGetter<number | readonly number[]> Visible-ratio thresholds that trigger the callback.
immediate? boolean Start observing during composable creation. When false, call resume.
host? MaybeRefOrGetter<IntersectionObserverHost | null | undefined> Reactive constructor capability for alternate runtimes and tests.
flush? "pre" | "post" | "sync" Target resolution timing. "post" observes template refs after mount.

IntersectionObserverControls

Controls returned by useIntersectionObserver.

Member Type Description
isSupported Readonly<Ref<boolean>> Whether the host provides IntersectionObserver. false during server rendering.
isActive Readonly<Ref<boolean>> Whether observation is currently requested (not paused or stopped).
pause () => void Disconnect the observer while keeping the ability to resume.
resume () => void Reconnect after pause (or start when created with immediate: false).
stop () => void Disconnect permanently. Later resume calls are ignored. Idempotent.

UseElementVisibilityOptions

Options for useElementVisibility.

Member Type Description
initialValue? boolean Visibility exposed before the first observation and during server rendering. Keep it identical on server and client.
once? boolean Stop observing after the element becomes visible for the first time.

ElementVisibilityControls

Reactive state returned by useElementVisibility.

Member Type Description
isVisible Readonly<Ref<boolean>> Whether the element currently intersects the root.
isSupported Readonly<Ref<boolean>> Whether the host provides IntersectionObserver.
stop () => void Stop observing. Idempotent.