Skip to content

use-intersection-observer

A React hook that provides a declarative way to observe element visibility using the Intersection Observer API with automatic cleanup and type-safe manner.

Browser Support

Important

This hook automatically checks for IntersectionObserver support and logs a warning in development if it's not available. The hook will gracefully handle unsupported browsers by not creating observers.

INFO

IntersectionObserver is supported in all modern browsers. For older browsers, you may need to include a polyfill.

Features

  • Auto cleanup: Observer is automatically disconnected on unmount or element changes
  • Reactive: The hook re-evaluates and potentially re-creates observers when dependencies change
  • Type-safe: Full TypeScript support with dynamic property naming based on key
  • Flexible keys: Support for custom property naming through the key parameter
  • Standard options: Full support for all IntersectionObserverInit options (root, rootMargin, threshold)
  • Performance: Observer is only created when element exists and IntersectionObserver is supported
  • One-time observation: Built-in support for observing elements only once

Parameters

ParameterTypeRequiredDefault ValueDescription
optionsIntersectionObserverOptionsundefinedConfiguration object for the intersection observer

Types

ts
export interface BaseIntersectionObserverOptions {
   onIntersection?: (entry: IntersectionObserverEntry) => void
   onlyTriggerOnce?: boolean
}

export interface IntersectionObserverOptions<Key extends string = ''>
   extends IntersectionObserverInit,
      BaseIntersectionObserverOptions {
   key?: Key
}

export type IntersectionObserverResult<Key extends string> = {
   // Dynamic property names based on key
   [K in Key as Key extends '' ? 'element' : `${Key}Element`]: HTMLElement | null
} & {
   [K in Key as Key extends '' ? 'setElementRef' : `set${Capitalize<Key>}ElementRef`]: (
      elementNode: HTMLElement | null
   ) => void
} & {
   [K in Key as Key extends '' ? 'isElementIntersecting' : `is${Capitalize<Key>}ElementIntersecting`]: boolean
}

Options Properties

PropertyTypeDefaultDescription
keystring''Custom key for property naming
onIntersection(entry: IntersectionObserverEntry) => voidundefinedCallback fired on intersection changes
onlyTriggerOncebooleanfalseWhether to observe only the first intersection
rootElement | Document | nullnullRoot element for intersection
rootMarginstring'0px'Margin around root element
thresholdnumber | number[]0Intersection ratio threshold(s)

Return Value

The hook returns an object with dynamically named properties based on the key parameter:

  • Without key: element, setElementRef, isElementIntersecting
  • With key: {key}Element, set{Key}ElementRef, is{Key}ElementIntersecting

INFO

element: Holds the element reference which is being observed, it's initial undefined.

setElementRef: Setter function to store the element reference within element, which is going tobe observed.

isElementIntersecting: Holds the boolean intersection status of the element weather it is intersecting the screen or not.

Usage Examples

Basic Intersection Observer

tsx
import { useIntersectionObserver } from 'classic-react-hooks'

export default function BasicExample() {
   const { element, setElementRef, isElementIntersecting } = useIntersectionObserver({
      threshold: 0.5,
      onIntersection: (entry) => {
         console.log('Intersection changed:', entry.isIntersecting)
      },
   })

   return (
      <div style={{ height: '200vh' }}>
         <div style={{ marginTop: '100vh' }}>
            <div
               ref={setElementRef}
               style={{
                  padding: '20px',
                  backgroundColor: isElementIntersecting ? 'lightgreen' : 'lightcoral',
               }}
            >
               {isElementIntersecting ? 'Visible!' : 'Not visible'}
            </div>
         </div>
      </div>
   )
}

Using Custom Keys

Example
tsx
import { useIntersectionObserver } from 'classic-react-hooks'

export default function CustomKeyExample() {
   const { heroElement, setHeroElementRef, isHeroElementIntersecting } = useIntersectionObserver({
      key: 'hero', 
      threshold: 0.3,
      rootMargin: '-50px',
   })

   return (
      <div>
         <header
            ref={setHeroElementRef}
            style={{
               height: '400px',
               backgroundColor: isHeroElementIntersecting ? 'blue' : 'gray',
               color: 'white',
               display: 'flex',
               alignItems: 'center',
               justifyContent: 'center',
            }}
         >
            <h1>Hero Section {isHeroElementIntersecting ? '(Visible)' : '(Hidden)'}</h1>
         </header>
         <div style={{ height: '200vh', padding: '20px' }}>
            <p>Scroll to see the hero section intersection status change</p>
         </div>
      </div>
   )
}

One-Time Trigger

Example
tsx
import { useState } from 'react'
import { useIntersectionObserver } from 'classic-react-hooks'

export default function OneTimeExample() {
   const [hasBeenSeen, setHasBeenSeen] = useState(false)

   const { setElementRef, isElementIntersecting } = useIntersectionObserver({
      onlyTriggerOnce: true, 
      threshold: 0.8,
      onIntersection: (entry) => {
         if (entry.isIntersecting) {
            setHasBeenSeen(true)
            console.log('Element seen for the first time!')
         }
      },
   })

   return (
      <div style={{ height: '200vh' }}>
         <div style={{ marginTop: '150vh' }}>
            <div
               ref={setElementRef}
               style={{
                  padding: '40px',
                  backgroundColor: hasBeenSeen ? 'gold' : 'lightblue',
                  textAlign: 'center',
               }}
            >
               {hasBeenSeen ? 'I was seen! 🎉' : 'Scroll down to see me'}
            </div>
         </div>
      </div>
   )
}

Multiple Thresholds

Example
tsx
import { useIntersectionObserver } from 'classic-react-hooks'

export default function MultipleThresholdsExample() {
   const { setElementRef, isElementIntersecting } = useIntersectionObserver({
      threshold: [0, 0.25, 0.5, 0.75, 1.0], 
      onIntersection: (entry) => {
         const percentage = Math.round(entry.intersectionRatio * 100)
         console.log(`Element is ${percentage}% visible`)
      },
   })

   return (
      <div style={{ height: '200vh', padding: '20px' }}>
         <div style={{ height: '100vh' }} />
         <div
            ref={setElementRef}
            style={{
               height: '300px',
               backgroundColor: isElementIntersecting ? 'lightgreen' : 'lightcoral',
               display: 'flex',
               alignItems: 'center',
               justifyContent: 'center',
            }}
         >
            <h2>Check console for intersection percentage</h2>
         </div>
      </div>
   )
}

Common Use Cases

  • Lazy loading: Load images or content when they come into view
  • Animation triggers: Start animations when elements become visible
  • Analytics: Track when users view certain sections
  • Infinite scrolling: Load more content when reaching the end
  • Sticky navigation: Show/hide navigation based on hero section visibility
  • Performance optimization: Pause expensive operations when elements are not visible

Performance Notes

INFO

  • The hook uses useSyncedRef to avoid unnecessary re-renders when callback functions change
  • Observer instances are automatically cleaned up and recreated only when necessary
  • The onlyTriggerOnce option helps optimize performance by automatically disconnecting after first intersection

TypeScript Benefits

The hook provides excellent TypeScript support:

  • Dynamic property names: Property names change based on the key parameter
  • Type inference: Return types are automatically inferred from the key
  • Full IntersectionObserver API support: All standard options are typed correctly
tsx
// Without key
const { element, setElementRef, isElementIntersecting } = useIntersectionObserver()

// With key 'sidebar'
const { sidebarElement, setSidebarElementRef, isSidebarElementIntersecting } = useIntersectionObserver({
   key: 'sidebar',
})