IntersectionObserver - Observes HTML elements when they are visible via JavaScript scrolling.
Content Index
- What is IntersectionObserver and why does it make visibility detection easier
- What can you detect with it?
- root
- rootMargin
- threshold
- entries
- How does Intersection Observer work?
- 1. Define the options
- 2. Create the observer
- 3. Select the elements
- 4. Observe them
- Practical example: activating links according to the visible heading
- 1. Select the headings of the post
- 2. Adjust the visibility zone
- 3. Create the observer
- 4. Observe each heading
- Why does this technique work so well?
- Advanced tips and best practices
- Frequently Asked Questions
- Conclusion
The IntersectionObserver is one of those JavaScript APIs that, when you discover it, you wonder how you survived using only scroll events. In my case, it allowed me to automate something I always did manually: detecting which heading is visible in a post and activating the corresponding link in the table of contents.
Since then, I haven't looked back.
In this guide, I explain how it works, how to configure it correctly, and how to apply it to a real-world internal navigation case (scroll-spy) using the exact same pattern I use in my projects.
I'm going to explain a very interesting method known as Intersection Observer in JavaScript. Basically, it allows me to implement this type of behavior: adding an observer over a list of IDs in a viewer and marking a link as activated based on a heading that is visible in the viewport.
What is IntersectionObserver and why does it make visibility detection easier
IntersectionObserver is an API that allows you to execute a function when an element enters or leaves the viewport (or any container you define).
And it does so without using constant scroll events, which means:
- better performance,
- cleaner code,
- and more accurate detections.
Before, you had to measure positions with getBoundingClientRect() and that was repeated on every scroll. Now the browser does the dirty work for you.
What can you detect with it?
- If an image is about to appear → lazy-loading.
- If a block is already visible → animations or analytics.
- If an element is 100% inside the view → activating a section.
- If a heading enters at 60% visibility → as I do, activating an index link.
Key Concepts: root, rootMargin, threshold, and entries
Although the API is simple, it's worth internalizing these four concepts because they are the foundation of everything.
root
It is the container whose visibility you will use as a reference.
- If it is null: the browser's viewport.
- If you define it: any scrollable div.
rootMargin
It is like CSS margin but applied to the visible area of the root.
It allows you to "advance" or "delay" the detection zone.
I, for example, use rootMargin: '0px 0px -60% 0px' because I want a heading to be considered visible when it is already further up the viewport, not just when it appears.
threshold
It is the percentage of visibility that the element must reach for the callback to fire.
It can be:
- a number (0.6 for example),
- or an array of numbers ([0, 0.25, 0.5, 1]).
For example:
- threshold: 1 → only when the element is totally visible
- threshold: 0 → as soon as it touches the edge
- threshold: 0.6 → this is the case I usually use
entries
It is an array that your callback receives every time there are changes.
Each entry contains:
- entry.target: observed element
- entry.isIntersecting: whether it is intersecting
- entry.intersectionRatio: visible percentage
- entry.boundingClientRect
- entry.intersectionRect
- entry.rootBounds
It allows you to decide exactly what you want to do when an element crosses your target zone.
How does Intersection Observer work?
1. Define the options
const options = { root: null, rootMargin: '0px', threshold: 0.5, };2. Create the observer
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log(entry.target.id);
}
});
}, options);3. Select the elements
const elementos = document.querySelectorAll('.observado');4. Observe them
elementos.forEach(el => observer.observe(el));Practical example: activating links according to the visible heading
This is exactly the case where I use this API the most.
When I write an article with many <h2> and <h3> headings, it's great for me to detect which one is visible and activate the equivalent link in the side table of contents.
1. Select the headings of the post
const headings = Array.from(
document.querySelectorAll('.post h2, .post h3')
);This ensures that all my main headings are monitored for what the book viewer is.
2. Adjust the visibility zone
This is where my practical experience comes in:
In my projects, I consider a heading to be "active" when 60% of it has already passed through the viewport, so I use a negative rootMargin to move the trigger upwards.
I consider it observable when at least 60% of the content is visible in the viewport:
const observerOptions = {
rootMargin: '0px 0px -60% 0px', // Detectar cuando el título está más arriba
};You can adjust that value to what works best for you.
Select the elements to observe:
const headings = Array.from(document.querySelectorAll('.post h2, .post h3'));3. Create the observer
In my HTML document (for example, in a post), I want to observe the h2 and h3 elements.
That is, when one of those headings reaches that 60% visibility, the desired code is executed.
Then, we iterate over those elements with a forEach and apply the .observe() to start monitoring them:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const id = entry.target.id;
// Mark the link as active
this.hxs.forEach(link => {
if (link.id === `${id}`) {
// TO DO
}
});
}
});
}, observerOptions);In my case:
- I check if there are entries in the entries array.
- If there are, I get the id of the heading.
- I look for the equivalent link.
- With that id, I do what I need.
- I apply an active style to it.
4. Observe each heading
Before using it, we need to register the observer. So far, we have only defined:
headings.forEach(h => observer.observe(h));Why does this technique work so well?
- It avoids flickering.
- It doesn't depend on scroll, but on real visibility.
- It is ultra efficient.
- It detects headings even if they jump quickly through the viewport.
Advanced tips and best practices
- Avoid observing too many elements unnecessarily
- The API is efficient, but don't overdo it.
- Observe only what you really need.
- Use threshold arrays when you want smooth animations
- If you need to detect dynamic visibility increments:
threshold: [0, 0.1, 0.2, ..., 1]
- Be careful with isIntersecting
- In some browsers, it doesn't behave the same if the threshold does not include 0.
- If you want total precision when an element is completely visible, it is better to use intersectionRatio.
- Do not use IntersectionObserver for complex logic on every frame
- If you need pixel-by-pixel animations, use requestAnimationFrame() instead.
Frequently Asked Questions
- Does IntersectionObserver consume less than listening for scroll?
- Yes, much less. Detection does not depend on continuous events.
- Can I observe many elements at once?
- Yes. A single observer can handle hundreds of elements.
- What is the difference between threshold and intersectionRatio?
- threshold is when you want the callback to fire.
intersectionRatio is the actual visible percentage at any given moment.
- threshold is when you want the callback to fire.
- Is it useful for a scroll-spy menu?
- Yes, in fact, it is my favorite practical case.
Conclusion
IntersectionObserver is an extremely powerful and very undervalued tool as its use is not very friendly and it is somewhat complex to understand and abstract; although I hope this guide allows you to understand it more easily and be able to see its potential and use it in your projects.
For my particular case, using it to activate links according to the visible heading has greatly simplified the navigation of my articles: zero calculations, zero scroll listeners, maximum precision.
I agree to receive announcements of interest about this Blog.
"What is IntersectionObserver in JavaScript? Explained clearly," Smart Scrolling, The API that will change the way you scroll the web.