Skip to main content

Overview

The inView() function detects when elements enter and leave the viewport. It provides a simple callback-based API built on the Intersection Observer API.

Import

import { inView } from 'framer-motion'

Signature

function inView(
  elementOrSelector: ElementOrSelector,
  onStart: ViewChangeHandler,
  options?: InViewOptions
): VoidFunction

type ElementOrSelector = Element | Element[] | string

type ViewChangeHandler = (
  element: Element,
  entry: IntersectionObserverEntry
) => void | ViewChangeHandler

interface InViewOptions {
  root?: Element | Document
  margin?: string
  amount?: "some" | "all" | number
}

Parameters

elementOrSelector (required)

The element(s) to observe. Can be a single element, array of elements, or a CSS selector.
elementOrSelector: Element | Element[] | string
// Single element
const element = document.querySelector('.box')
inView(element, (el) => {
  console.log('Element entered view')
})

// CSS selector
inView('.box', (el) => {
  console.log('Element entered view')
})

// Multiple elements
const elements = document.querySelectorAll('.box')
inView(elements, (el) => {
  console.log('Element entered view')
})

onStart (required)

Callback function that fires when the element enters the viewport.
onStart: (element: Element, entry: IntersectionObserverEntry) =>
  void | ViewChangeHandler
Return a function to be called when the element leaves the viewport:
inView('.box', (element, entry) => {
  console.log('Entered view')
  element.classList.add('visible')
  
  // Return cleanup function for when it leaves view
  return (exitEntry) => {
    console.log('Left view')
    element.classList.remove('visible')
  }
})

root

The scrolling container. Defaults to the viewport.
root?: Element | Document
const container = document.querySelector('.scroll-container')

inView('.box', (el) => {
  console.log('In view')
}, { root: container })

margin

Margin around the root element. Similar to CSS margin.
margin?: string
Accepts values like:
  • "0px"
  • "0px 100px"
  • "0px 100px -50px 0px"
inView('.box', (el) => {
  console.log('In view')
}, {
  margin: "0px 0px -100px 0px" // Trigger 100px before element is visible
})

amount

How much of the element must be visible.
amount?: "some" | "all" | number // default: "some"
  • "some" (default): Any part visible triggers
  • "all": Entire element must be visible
  • number: Percentage from 0 to 1
// Trigger when 50% visible
inView('.box', (el) => {
  console.log('50% visible')
}, { amount: 0.5 })

// Trigger when completely visible
inView('.box', (el) => {
  console.log('Fully visible')
}, { amount: "all" })

Return Value

Returns a cleanup function to stop observing.
VoidFunction
const stopObserving = inView('.box', (el) => {
  console.log('In view')
})

// Later, stop observing
stopObserving()

Examples

Basic usage

import { inView } from 'framer-motion'

inView('.fade-in', (element) => {
  element.style.opacity = '1'
  element.style.transform = 'translateY(0)'
})

Enter and exit animations

inView('.animated', (element) => {
  element.classList.add('visible')
  
  return () => {
    element.classList.remove('visible')
  }
})

Trigger once

inView('.fade-in', (element) => {
  element.style.opacity = '1'
  // Don't return a function - won't observe exit
})

Stagger multiple elements

inView('.stagger-item', (element, entry) => {
  const index = Array.from(
    document.querySelectorAll('.stagger-item')
  ).indexOf(element)
  
  setTimeout(() => {
    element.style.opacity = '1'
    element.style.transform = 'translateY(0)'
  }, index * 100)
})

With animation library

import { animate, inView } from 'framer-motion'

inView('.box', (element) => {
  const animation = animate(
    element,
    { opacity: [0, 1], y: [50, 0] },
    { duration: 0.5 }
  )
  
  return () => {
    animate(
      element,
      { opacity: 0, y: 50 },
      { duration: 0.3 }
    )
  }
})

Lazy load images

inView('img[data-src]', (img) => {
  const src = img.getAttribute('data-src')
  img.setAttribute('src', src)
  img.removeAttribute('data-src')
})

Video autoplay

inView('video[data-autoplay]', (video) => {
  video.play()
  
  return () => {
    video.pause()
  }
})

Scroll-triggered counter

inView('.counter', (element) => {
  const target = parseInt(element.dataset.target)
  let current = 0
  const increment = target / 100
  
  const timer = setInterval(() => {
    current += increment
    if (current >= target) {
      element.textContent = target
      clearInterval(timer)
    } else {
      element.textContent = Math.floor(current)
    }
  }, 20)
  
  return () => clearInterval(timer)
})

Container scrolling

const container = document.querySelector('.scroll-container')

inView('.item', (element) => {
  element.style.opacity = '1'
  
  return () => {
    element.style.opacity = '0.3'
  }
}, {
  root: container,
  amount: 0.5
})

Track analytics

inView('.section', (element, entry) => {
  const sectionName = element.dataset.section
  
  // Track when section becomes visible
  analytics.track('Section Viewed', {
    section: sectionName,
    timestamp: Date.now()
  })
  
  return (exitEntry) => {
    // Track when section leaves view
    analytics.track('Section Left', {
      section: sectionName,
      timestamp: Date.now()
    })
  }
}, { amount: 0.5 })

Progressive disclosure

const stopObserving = inView('.reveal', (element) => {
  const delay = parseInt(element.dataset.delay || '0')
  
  setTimeout(() => {
    element.classList.add('revealed')
  }, delay)
})

// Clean up on page unload
window.addEventListener('unload', stopObserving)

IntersectionObserverEntry

The entry object passed to callbacks contains:
interface IntersectionObserverEntry {
  boundingClientRect: DOMRectReadOnly
  intersectionRatio: number
  intersectionRect: DOMRectReadOnly
  isIntersecting: boolean
  rootBounds: DOMRectReadOnly | null
  target: Element
  time: number
}

Notes

  • Uses native Intersection Observer API
  • Automatically handles cleanup when elements are removed
  • Observes all matching elements when using a selector
  • More performant than scroll event listeners
  • Well supported in modern browsers

Build docs developers (and LLMs) love