PatternHookScrollVisibility
Intersection Observer Hook
A hook for detecting when elements enter or leave the viewport. Useful for lazy loading, infinite scroll, and scroll-triggered animations.
Preview
Box 1: HiddenBox 2: HiddenBox 3: Hidden
Box 1
Box 2
Box 3
Scroll the container to see visibility change
Code
import { useState, useEffect, useRef, RefObject } from 'react'
interface IntersectionOptions {
threshold?: number | number[]
root?: Element | null
rootMargin?: string
freezeOnceVisible?: boolean
}
export function useIntersectionObserver<T extends Element>(
options: IntersectionOptions = {}
): [RefObject<T>, boolean, IntersectionObserverEntry | null] {
const {
threshold = 0,
root = null,
rootMargin = '0px',
freezeOnceVisible = false,
} = options
const ref = useRef<T>(null)
const [isVisible, setIsVisible] = useState(false)
const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null)
useEffect(() => {
const element = ref.current
if (!element) return
// Skip if frozen and already visible
if (freezeOnceVisible && isVisible) return
const observer = new IntersectionObserver(
([entry]) => {
setEntry(entry)
setIsVisible(entry.isIntersecting)
},
{ threshold, root, rootMargin }
)
observer.observe(element)
return () => {
observer.disconnect()
}
}, [threshold, root, rootMargin, freezeOnceVisible, isVisible])
return [ref, isVisible, entry]
}
// Usage example:
function LazyImage({ src, alt }: { src: string; alt: string }) {
const [ref, isVisible] = useIntersectionObserver<HTMLDivElement>({
threshold: 0.1,
freezeOnceVisible: true,
})
return (
<div ref={ref} className="min-h-[200px]">
{isVisible ? (
<img src={src} alt={alt} className="animate-fade-in" />
) : (
<div className="bg-gray-200 animate-pulse h-full" />
)}
</div>
)
}Looking for more?
Browse the full collection of patterns or check out other exploration topics.