|$ curl https://forge-ai.dev/api/markdown?path=docs/web-platform/intersection-observer
$cat docs/intersection-observer.md
updated Recently·22 min read·published

Intersection Observer

Web PlatformIntermediate🎯Free Tools
Introduction

The Intersection Observer API asynchronously observes changes in the intersection of a target element with its ancestor (or viewport). It replaces expensive scroll-event listeners for tasks like lazy loading, infinite scroll, and visibility tracking — running off the main thread.

Basic Usage
basic-observer.ts
TypeScript
1// Create an observer
2const observer = new IntersectionObserver(
3 (entries) => {
4 entries.forEach((entry) => {
5 if (entry.isIntersecting) {
6 console.log("Element is visible:", entry.target);
7 } else {
8 console.log("Element is hidden:", entry.target);
9 }
10 });
11 },
12 {
13 root: null, // viewport
14 rootMargin: "0px", // no margin
15 threshold: 0, // trigger when any part is visible
16 }
17);
18
19// Observe elements
20const elements = document.querySelectorAll(".observe-me");
21elements.forEach((el) => observer.observe(el));
22
23// Stop observing
24observer.unobserve(element);
25
26// Disconnect all
27observer.disconnect();
thresholds.ts
TypeScript
1// Thresholds: trigger at specific visibility percentages
2const observer = new IntersectionObserver(callback, {
3 threshold: [0, 0.25, 0.5, 0.75, 1],
4 // Triggers at 0%, 25%, 50%, 75%, 100% visibility
5});
6
7// Root margin: expand/contract the observation area
8const observer = new IntersectionObserver(callback, {
9 rootMargin: "100px 0px", // 100px above and below viewport
10 // Triggers 100px before element enters viewport
11});
12
13// Negative root margin: shrink the trigger area
14const observer = new IntersectionObserver(callback, {
15 rootMargin: "-50px 0px", // Trigger when 50px inside viewport
16});
Lazy Loading
lazy-loading.ts
TypeScript
1// Lazy load images
2const imageObserver = new IntersectionObserver(
3 (entries) => {
4 entries.forEach((entry) => {
5 if (entry.isIntersecting) {
6 const img = entry.target as HTMLImageElement;
7 img.src = img.dataset.src!; // Load actual image
8 img.classList.add("loaded");
9 imageObserver.unobserve(img);
10 }
11 });
12 },
13 { rootMargin: "200px 0px" } // Start loading 200px before visible
14);
15
16document.querySelectorAll("img[data-src]").forEach((img) => {
17 imageObserver.observe(img);
18});
19
20// HTML: <img data-src="/photo.jpg" src="/placeholder.jpg" alt="..." />
21
22// Lazy load components (React)
23function LazySection({ children }: { children: React.ReactNode }) {
24 const ref = useRef<HTMLDivElement>(null);
25 const [isVisible, setIsVisible] = useState(false);
26
27 useEffect(() => {
28 const observer = new IntersectionObserver(
29 ([entry]) => { if (entry.isIntersecting) setIsVisible(true); },
30 { rootMargin: "200px" }
31 );
32 if (ref.current) observer.observe(ref.current);
33 return () => observer.disconnect();
34 }, []);
35
36 return <div ref={ref}>{isVisible ? children : <div className="h-96" />}</div>;
37}
Infinite Scroll
infinite-scroll.ts
TypeScript
1// Infinite scroll with Intersection Observer
2function useInfiniteScroll(loadMore: () => Promise<void>) {
3 const sentinelRef = useRef<HTMLDivElement>(null);
4
5 useEffect(() => {
6 const observer = new IntersectionObserver(
7 async ([entry]) => {
8 if (entry.isIntersecting) {
9 await loadMore();
10 }
11 },
12 { rootMargin: "200px" }
13 );
14
15 if (sentinelRef.current) observer.observe(sentinelRef.current);
16 return () => observer.disconnect();
17 }, [loadMore]);
18
19 return sentinelRef;
20}
21
22// Usage
23function Feed() {
24 const [items, setItems] = useState<Item[]>([]);
25 const [hasMore, setHasMore] = useState(true);
26
27 const loadMore = useCallback(async () => {
28 if (!hasMore) return;
29 const newItems = await fetchItems(items.length);
30 setItems((prev) => [...prev, ...newItems]);
31 setHasMore(newItems.length > 0);
32 }, [items.length, hasMore]);
33
34 const sentinelRef = useInfiniteScroll(loadMore);
35
36 return (
37 <div>
38 {items.map((item) => <FeedItem key={item.id} item={item} />)}
39 {hasMore && <div ref={sentinelRef} className="h-10" />}
40 </div>
41 );
42}

info

Add a rootMargin of 200-400px to start fetching before the sentinel is visible. This masks network latency and creates a seamless infinite scroll experience.
$Blueprint — Engineering Documentation·Section ID: WP-IO-01·Revision: 1.0