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
Observe a featured guide inside its own viewport
Measure whether a featured card crosses a real 60 percent visibility threshold and expose pause, resume, and permanent stop.
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


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. |