
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.
<script setup>
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>API
| Composable | Description |
|---|---|
| useResizeObserver→ | Observe element size changes |
| useMutationObserver→ | Observe DOM mutations |
| useEventListener→ | General event handling |
useIntersectionObserver
Type
interface IntersectionObserverOptions { immediate?: boolean root?: Element | null rootMargin?: string threshold?: number | number[] } function useIntersectionObserver( target: Ref<Element | undefined>, callback: (entries: IntersectionObserverEntry[]) => void, options?: IntersectionObserverOptions ): { isActive: Readonly<Ref<boolean>> isIntersecting: Readonly<Ref<boolean>> isPaused: Readonly<Ref<boolean>> pause: () => void resume: () => void stop: () => void }Details
Observes when an element intersects with the viewport or a specified ancestor element. Automatically handles cleanup on component unmount.
Parameters
target: Ref to the element to observecallback: Function called when intersection changesoptions:immediate: Trigger callback immediately with synthetic entry (default: false)root: The element used as viewport for checking visibility (default: null - uses viewport)rootMargin: Margin around the root, e.g., ‘10px 20px 30px 40px’ (default: ‘0px’)threshold: Visibility percentage(s) that trigger the callback, 0-1 or array (default: 0)
Returns
isActive: Whether the observer is currently created and observingisIntersecting: Whether the element is currently intersectingisPaused: Whether observation is pausedpause(): Pause observationresume(): Resume observationstop(): Stop observation permanently
Example
const element = useTemplateRef('element') const { isIntersecting, pause, resume } = useIntersectionObserver( element, ([entry]) => { console.log('Intersection ratio:', entry.intersectionRatio) }, { threshold: [0, 0.5, 1], // Trigger at 0%, 50%, and 100% rootMargin: '100px' // Start observing 100px before entering } )
useElementIntersection
Type
function useElementIntersection( target: Ref<Element | undefined>, options?: IntersectionObserverOptions ): { isActive: Readonly<Ref<boolean>> isIntersecting: Readonly<Ref<boolean>> intersectionRatio: Readonly<Ref<number>> isPaused: Readonly<Ref<boolean>> pause: () => void resume: () => void stop: () => void }Details
Convenience function for tracking element intersection state reactively without a callback.
Returns
isActive: Whether the observer is currently created and observingisIntersecting: Whether the element is currently intersectingintersectionRatio: How much of the element is visible (0-1)isPaused: Whether observation is pausedpause(): Pause observationresume(): Resume observationstop(): Stop observation permanently
Example
const image = useTemplateRef('image') const { isIntersecting } = useElementIntersection(image, { threshold: 0.1 }) // Use isIntersecting in template or watch watch(isIntersecting, (visible) => { if (visible) loadImage() })
Lifecycle & Cleanup
Automatic Cleanup
useIntersectionObserver automatically disconnects the observer when:
- The component unmounts
- The Vue effect scope is disposed
- You call the returned
stop()function
Implementation:
// Uses Vue's onScopeDispose internally
onScopeDispose(() => observer.disconnect())This prevents memory leaks by ensuring observers don’t continue running after the component is destroyed.
Manual Control
The composable returns control functions for fine-grained lifecycle management:
const { isActive, isIntersecting, pause, resume, stop } = useIntersectionObserver(
element,
callback
)
// Check if observer is active
console.log(isActive.value) // true
// Temporarily pause observation (keeps observer alive)
pause()
console.log(isActive.value) // false
console.log(isIntersecting.value) // false (reset on pause)
// Resume observation
resume()
console.log(isActive.value) // true
// Permanently stop and disconnect observer
stop()
console.log(isActive.value) // falseState properties:
isActive: True when the observer exists and is observing (false when paused or stopped)isPaused: True when observation is temporarily pausedisIntersecting: True when the element is currently intersecting the viewport
Difference between pause and stop:
pause(): Temporarily stops observing, can be resumed withresume()stop(): Permanently disconnects the observer, cannot be restarted
Reactive Target
The target element can be reactive. When the target ref changes, the observer automatically re-attaches:
const element = ref<HTMLElement | null>(null)
useIntersectionObserver(element, callback)
// Later - observer automatically reconnects to new element
element.value = document.querySelector('.new-target')Template Refs
Works seamlessly with Vue’s template refs:
<script setup>
import { useTemplateRef } from 'vue'
import { useIntersectionObserver } from '@vuetify/v0'
const section = useTemplateRef('section')
const { isIntersecting } = useIntersectionObserver(
section,
([entry]) => {
console.log('Section visibility:', entry.isIntersecting)
}
)
</script>
<template>
<section ref="section">
<p v-if="isIntersecting">Now visible!</p>
</section>
</template>Usage Outside Components
If called outside a component setup function:
- No automatic cleanup (no active effect scope)
- Must manually call
stop()to prevent memory leaks - Consider wrapping in
effectScope():
import { effectScope } from 'vue'
const scope = effectScope()
scope.run(() => {
useIntersectionObserver(element, callback)
})
// Later, cleanup all observers in the scope
scope.stop()SSR Considerations
IntersectionObserver is a browser-only API. The composable checks for browser environment internally:
// Safe to call during SSR - will not throw
const { isIntersecting } = useIntersectionObserver(element, callback)
// isIntersecting.value will be false in SSRPerformance Tips
Use appropriate thresholds:
// Trigger once when element appears
useIntersectionObserver(element, callback, { threshold: 0 })
// Trigger at multiple visibility levels
useIntersectionObserver(element, callback, { threshold: [0, 0.25, 0.5, 0.75, 1] })Use rootMargin for early loading:
// Start loading 200px before element enters viewport
useIntersectionObserver(element, callback, {
rootMargin: '200px'
})Pause when not needed:
const { pause, resume } = useIntersectionObserver(element, callback)
// Pause during heavy operations
pause()
performHeavyWork()
resume()