Intersection Observer: react when elements scroll into view
Scroll listeners that measure element positions are slow and fiddly. Intersection Observer tells you when an element enters the viewport, with a few lines of code.
Knowing when an element becomes visible used to mean listening to the scroll event and calling getBoundingClientRect() on every element, many times a second. That work runs on the main thread, forces layout and makes scrolling feel heavy.
The Intersection Observer API turns it around. You tell the browser which elements you care about, and it calls you when their visibility changes.
The basic pattern
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
entry.target.classList.add("is-visible");
observer.unobserve(entry.target); // one time only
}
},
{ rootMargin: "0px 0px -10% 0px", threshold: 0.1 },
);
document.querySelectorAll("[data-reveal]").forEach((el) => observer.observe(el));
Each element gets the is-visible class the first time it scrolls into view, and is then dropped from the watch list. This site uses the same pattern for its scroll reveals.
The options
| Option | Meaning | Example |
|---|---|---|
root |
The scrolling container to watch. Defaults to the viewport. | A scrollable panel |
rootMargin |
Grows or shrinks the area that counts as visible | "400px" to trigger early |
threshold |
How much of the element must be visible | 0.1 for 10 percent, 1 for all of it |
Useful things to build with it
Infinite scroll. Watch one empty element at the end of the list and load the next page as it approaches.
const sentinel = document.querySelector("#load-more");
new IntersectionObserver(
async ([entry]) => {
if (entry.isIntersecting) await loadNextPage();
},
{ rootMargin: "400px" },
).observe(sentinel);
Lazy loaded components. Load a map, a comment section or a video player only when the user gets near it.
Pausing work off screen. Stop a video or an animation that nobody can see, and resume it when it returns.
Impression tracking. Record that a banner was seen, with a threshold that matches your definition of seen.
Sticky header states. Observe a small element at the top of the page to know when the user has scrolled past it.
Use the simpler tool for images
For plain images you do not need JavaScript at all. The loading="lazy" attribute does the job in every modern browser. See native image lazy loading. Keep Intersection Observer for cases where code has to run.
Things to know
- The callback fires once right after
observe(), with the element's current state. CheckisIntersectingand do not assume it. - Call
unobserve()ordisconnect()when you are done, especially in single page apps where components come and go. - It reports changes asynchronously. Do not use it for animation that has to track the scroll position exactly.
- If you use it for reveal animations, respect
prefers-reduced-motionand show the content without animating.
The takeaway
Intersection Observer replaces a whole class of scroll handlers with a few lines that the browser runs efficiently. If your code listens to scroll to find out what is visible, this is the replacement.