chan.dev / posts

Intersection Observer

A planet with overlapping haves appearing to be fused together.

🌱 This post is in the growth phase. It may still be useful as it grows up.

Contents

Minimum usage

new IntersectionObserver(calledOnIntersection).observe(
childElement
)

Instantiate Intersection Observer with options

new IntersectionObserver(calledOnIntersection, {})

Root option

root sets the containing elements. Its default is the top-level viewport.

new IntersectionObserver(calledOnIntersection, {
root: document.querySelector('#target_scroll_area'),
})

Threshold option

threshold sets the amount of intersection the observed child must have with the root to trigger the callback. Its default is 0 (any intersection) and can be any decimal between 0 and 1.

Illustraction of intersections with various threshold. With a value of 0, any overlapping target element is green. With a value of 0.5, only elements overlapping by 50% or more are green. And with a value of 1, only elemnets that are 100% in view are green.

new IntersectionObserver(calledOnIntersection, {
root: document.querySelector('#target_scroll_area'),
threshold: 0.5, // 50% intersection to be "in view"
})

Margin option

Illustration of intersections with various margin values. With a value of 0, the viewport is used to determine intersecting targets. With a value of 16px, that area grows to be viewport + margin. With a value of -16 px, the area where intersections are evaluated is 16px inset from the viewport edge (on all sides).

margin accepts any CSS margin value (including negative values). Its default is 0.

new IntersectionObserver(calledOnIntersection, {
root: document.querySelector('#target_scroll_area'),
threshold: 0.5,
rootMargin: '-50px 0', // 50px of off-screen margin, only on y-axis.
})

IntersectionObserverEntry object

Callbacks receive an array of IntersectionObserverEntry objects.

let myIOCallback = (entries, observer) => {
entries.forEach((entry) => {
// entry: {
// boundingClientRect
// intersectionRatio
// intersectionRect
// isIntersecting
// rootBounds
// target
// time
// }
})
}

See these references for details on IntersectionObserverEntry:

.observe() method

Provides an IntersectionObserver instance with target elements to observe.

myIntersectionObserver.observe(target)

Observed multiple elements for intersections:

const descendents = document.querySelectorAll(
'.some-descendent'
)
myIntersectionObserver.observe(targetdescendents)

IntersectionObserver observe() method reference.

.disconnent()

Stops the IntersectionObserver instance from observing any targets.

myIntersectionObserver.disconnect()

IntersectionObserver disconnect() method reference.

.takeRecords()

Query an IntersectionObserver instance for all observed targets. Returns an array of IntersectionObserverEntry objects.

myIntersectionObserver.takeRecords()

IntersectionObserver takeRecords() method reference.

.unobserve(target)

Remove observed target (or targets) from an IntersectionObserver instance.

myIntersectionObserver.unobserve(target)

IntersectionObserver takeRecords() method reference.


Common uses for IntersectionObserver

  • ”Read Time” and “amount read” indicators.
  • Scrolly-telling animation triggers.
  • Lazy-loading assets and scripts.
    • Prefer more modern tools lazy and script’s defer async.
  • Closing out-of-view menus, tooltips, and overlays.
  • Ad interaction tracking.

When not to use Intersection Observers

Good counterpoints from Ori Livni.

Posts asking for help

Support

Strong: https://caniuse.com/intersectionobserver

Cool demo

Scroll-snap + sticky header

Scrolly-telling

There and back effect

CodePen collection by Ahmad Shadee

Top YouTube results for "intersection observer" in incognito.

</<div data-responsive-youtube-container> <iframe width="560" height="315" src="https://www.youtube.com/embed/2IbRtjez6ag?si=XtGevFRB6ceNN2X5" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe> </div> <div data-responsive-youtube-container> <iframe width="560" height="315" src="https://www.youtube.com/embed/T8EYosX4NOo?si=nlRy6jL73s2wuGKH" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe> </div> <div data-responsive-youtube-container> <iframe width="560" height="315" src="https://www.youtube.com/embed/aUjBvuUdkhg?si=0qbYvlh4U8Er8Z8d" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe> </div> <div data-responsive-youtube-container> <iframe width="560" height="315" src="https://www.youtube.com/embed/r1auJEf9ISo?si=fAuZbPw-Ly81uC0_" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe> </div>

Search by views count