Intersection Observer watches an element and tells you when it becomes visible inside the viewport or inside another scrollable container. Instead of checking scroll position over and over, you let the browser handle that work more efficiently.
It is useful for lazy loading images, triggering animations when blocks enter view, tracking when an ad or widget becomes visible, and loading more content when a user reaches the bottom of a list. For many real websites, it is cleaner and lighter than custom scroll listeners.
This example watches one box and adds a class the moment it comes into view.
<div class="fade-box" id="watchMe">Watch me</div>
<style>
.fade-box{
opacity:0;
transform:translateY(20px);
transition:opacity .35s ease, transform .35s ease;
}
.fade-box.is-visible{
opacity:1;
transform:none;
}
</style>
<script>
const box = document.getElementById('watchMe');
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
}
});
});
observer.observe(box);
</script>
A common pattern is storing the real image in data-src and only loading it when the image is near the viewport.
<img class="lazy-img" src="/images/placeholder.jpg" data-src="/images/photo.jpg" alt="Example">
<script>
const images = document.querySelectorAll('.lazy-img');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
});
}, {
rootMargin: '200px 0px'
});
images.forEach((img) => {
imageObserver.observe(img);
});
</script>
Instead of checking scroll position on every movement, watch a small trigger element near the bottom of the page.
<div id="postList"></div>
<div id="loadMoreTrigger"></div>
<script>
const trigger = document.getElementById('loadMoreTrigger');
let loading = false;
const loadObserver = new IntersectionObserver(async (entries) => {
const entry = entries[0];
if (!entry.isIntersecting || loading) return;
loading = true;
// fetch more posts here
// append posts into #postList
loading = false;
}, {
rootMargin: '300px 0px'
});
loadObserver.observe(trigger);
</script>
root lets you observe visibility inside a scrollable container instead of the whole window. rootMargin expands or shrinks the trigger area, which is useful when you want something to load before it actually appears. threshold controls how much of the element must be visible before the callback runs.
const observer = new IntersectionObserver(callback, {
root: null,
rootMargin: '100px 0px',
threshold: 0.25
});
Lazy loading images, fade-in sections, counting ad impressions, pausing videos when they leave view, tracking when users actually see a block, infinite scroll lists, and triggering sticky interface changes once a marker passes through the viewport.
Forgetting to call unobserve() when you only need a one-time trigger, using the observer for things that could be handled with normal CSS, watching too many elements with unnecessary complexity, or expecting it to replace every scroll-based interaction.
Use Intersection Observer when you care about whether something is visible or nearly visible. It is especially strong for content loading and viewport-triggered behavior. If you only need a sticky header or simple hover effect, it is probably overkill.
It helps you avoid noisy scroll handlers and keeps your code cleaner. For VibeScriptz-style pages and tools, it is a very practical browser feature because it solves real UI and performance problems without requiring a framework.
Read the full API reference on MDN Web Docs.
Resize Observer, Mutation Observer, lazy loading, infinite scroll, scroll-triggered animation, and content visibility would all connect well to this topic.