useIntersectionObserver
A composable for detecting when elements enter or leave the viewport using the Intersection Observer API with automatic cleanup.
Usage
The useIntersectionObserver composable wraps the Intersection Observer API to detect when elements become visible in the viewport. It’s useful for lazy loading images, infinite scroll, entrance animations, and performance optimizations.
Why wrap IntersectionObserver? The native IntersectionObserver has no awareness of Vue’s effectScope lifecycle. If you create one inside a composable, it won’t automatically disconnect when the scope is disposed. useIntersectionObserver integrates onScopeDispose for automatic cleanup, defers creation until after hydration for SSR safety, and adds reactive target tracking — things the native API can’t do on its own.
<script setup lang="ts">
import { useIntersectionObserver } from '@vuetify/v0'
import { ref, useTemplateRef } from 'vue'
const target = useTemplateRef('target')
const isVisible = ref(false)
useIntersectionObserver(target, (entries) => {
isVisible.value = entries[0].isIntersecting
}, {
threshold: 0.5, // Trigger when 50% visible
rootMargin: '0px'
})
</script>
<template>
<div>
<div style="height: 100vh">Scroll down to see the element</div>
<div ref="target" :class="{ visible: isVisible }">
I'm {{ isVisible ? 'visible' : 'hidden' }}
</div>
</div>
</template>Architecture
useIntersectionObserver wraps the native IntersectionObserver API with Vue reactivity:
Options
| Option | Type | Default | Notes |
|---|---|---|---|
immediate | boolean | false | Fire the callback immediately on mount before any intersection |
once | boolean | false | Stop observing after the first intersection fires |
threshold | number | number[] | 0 | Intersection ratio(s) at which the callback fires |
root | Element | null | null | Ancestor to use as viewport (null = browser viewport) |
rootMargin | string | '0px' | CSS margin around the root for intersection calculations |
Reactivity
| Property/Method | Reactive | Notes |
|---|---|---|
isActive | Computed from observer ref | |
isIntersecting | ShallowRef, readonly | |
isPaused | ShallowRef, readonly | |
target | Accepts MaybeRef, watched for changes | |
pause() | — | Temporarily stop observing without disconnecting |
resume() | — | Resume after pause() |
stop() | — | Disconnect the observer permanently |
useElementIntersection
| Property/Method | Reactive | Notes |
|---|---|---|
isIntersecting | ShallowRef, readonly | |
intersectionRatio | ShallowRef, readonly (0.0 to 1.0) |
Examples
Lazy Loading
Load images only when entering viewport
Infinite Scroll
Fetch more content as users scroll
Analytics
Track visibility for engagement metrics
Animations
Trigger entrance animations on scroll
Video Playback
Auto-play videos when visible
Ad Viewability
Measure ad impressions accurately
FAQ
Pass { once: true } and the observer disconnects after the first intersection. This avoids ongoing callback overhead for one-shot cases like lazy-loading an image or a single entrance animation.
useIntersectionObserver gives you the raw entries callback plus pause(), resume(), and stop() control. useElementIntersection is a thinner wrapper around a single element that just exposes reactive isIntersecting and intersectionRatio.
Pass the container element as the root option — it defaults to null, the browser viewport. rootMargin and threshold are then calculated relative to that container.
Pass an array of thresholds so the callback fires at each ratio step — e.g. Array.from({ length: 21 }, (_, i) => i / 20) fires every 5%. The scroll-reveal example uses exactly this to drive its progress bar.