Intersection Observer over Scroll Listener

Cover for Intersection Observer over Scroll Listener

I’m in the middle of revamping my site and wanted to add a small touch to the navbar. I want it to be sticky and show a border only after you scroll down a bit.

My first thought was to use a simple scroll listener. The code is pretty straightforward:

JavaScript
const nav = document.getElementById("navbar");

// Add a scroll event listener to the window
window.addEventListener("scroll", () => {
    // Check if the user has scrolled more than 40 pixels from the top
    if (window.scrollY > 40) {
        // If scrolled, add the 'scrolled' class
        nav?.classList.add("scrolled");
    } else {
        // If at the top, remove the 'scrolled' class
        nav?.classList.remove("scrolled");
    }
});

It worked, but there was a problem. The site felt janky, especially when I tried it on my iPad.

I looked for a better way and found the Intersection Observer API. It’s a much better tool for this kind of job.

The problem with a scroll listener is that it fires on every single pixel you scroll. This can cause performance issues and make animations feel sluggish because it runs on the main thread.

The Intersection Observer is different. It doesn’t fire constantly. Instead, it just tells you when an element enters or leaves the screen. This is way more efficient because the browser handles the hard work asynchronously.

So I decided to use it. The idea is to create an empty element at the top of the page to act as a trigger. When that trigger element is no longer visible on the screen, I know the user has scrolled.

First, I added this empty div right below the navbar.

HTML
<div id="navbar-scroll-trigger"></div>

Then, I wrote the JavaScript to observe it.

JavaScript
const nav = document.getElementById("navbar");
const trigger = document.getElementById("navbar-scroll-trigger");

const callback = (entries) => {
    entries.forEach((entry) => {
        // If the trigger is NOT intersecting (is off-screen)
        if (!entry.isIntersecting) {
            nav?.classList.add("scrolled");
        } else {
            nav?.classList.remove("scrolled");
        }
    });
};

const observer = new IntersectionObserver(callback);
observer.observe(trigger);

The logic is simple. The callback gets a list of entries, and each entry has a property called isIntersecting.

This property is true when the trigger element is visible on the screen and false when it’s not.

So, when the user scrolls down, the #navbar-scroll-trigger leaves the view, isIntersecting becomes false, and I add the scrolled class to my navbar. When they scroll back to the top, the trigger comes back into view, isIntersecting becomes true, and I remove the class.

The result is a much smoother experience with better performance. It’s a small change, but it’s great to learn a more modern and performant way to handle scroll effects.

Topics