惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

有赞技术团队
有赞技术团队
美团技术团队
博客园 - 司徒正美
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
阮一峰的网络日志
阮一峰的网络日志
S
SegmentFault 最新的问题
博客园_首页
雷峰网
雷峰网
V
V2EX
The Cloudflare Blog
博客园 - 三生石上(FineUI控件)
量子位
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
爱范儿
爱范儿
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 聂微东
V
Visual Studio Blog
Hugging Face - Blog
Hugging Face - Blog
博客园 - 【当耐特】
Jina AI
Jina AI
月光博客
月光博客
L
LangChain Blog

JavaScript iDiallo.com

Complex function call delays without nesting in JavaScript How to use pushState without breaking the back button How to create DOM elements efficiently with JavaScript Uncaught TypeError: Cannot read property of null Why Use Prototype in JavaScript Optimization: Minimize look-ups in for loops. Uncaught SyntaxError: Unexpected token < Magical JavaScript Live Collection Creating a class in JavaScript
How to tell an element is in view using JavaScript.
Ibrahim Dial · 2025-06-24 · via JavaScript iDiallo.com

Recently, I've created a dynamic story where I wanted an event to trigger when an element is in view. The solution was simple, but it's easy to make a mistake and drain your website's performance. Let's break it down into how we can create a flexible function that doesn't freeze the page. Note, modern browsers have a new API that makes this problem trivial, but first let's try to understand the problem and how to solve it. Skip to Modern Solution!

First, how do you know an element is in view? Let's create a function called isElementInView() that returns true or false when we pass an element.

const isElementInView = (element) => {
    return false;
}

Today, all major browsers support the getBoundingClientRect() method. This method returns the position of the element on the page. It includes the properties: left, top, right, bottom, x, y, width, and height. Using these properties and the current windows inner width and height, we can check if the element is in view.

const isElementInView = (el) => {
    if (!el) return false;
    const rect = el.getBoundingClientRect();
    const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
    const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
    return (
        rect.top >= 0 &&
        rect.left >= 0 &&
        rect.bottom <= viewportHeight &&
        rect.right <= viewportWidth
    );
};

When an element is in full view and we call the function isElementInView, it will return true. If it is not in view or only partially in view, the function will return false. Now it's nice that we can identify that an element is in view, this would only work if we call the function constantly. This can be achieved by hooking up this function with the scroll event.

Here is how:

window.addEventListener("scroll", () => {
    const element = document.getElementById("my-element");
    if (isElementInView(element)) {
        doSomething();
    }
});

Every time a user scrolls, even by a fraction of a pixel, the browser will trigger the event. If the element is in view then our function doSomething() will be called. There is a potential problem here. If you only want the function doSomething() to be called once, then we will have to make some modifications. For example, we can add a boolean to track if the function has been called already. Let's make that change.

let hasBeenCalled = false;
window.addEventListener("scroll", () => {
    if (hasBeenCalled) {
        return;
    }
    const element = document.getElementById("my-element");
    if (isElementInView(element)) {
        doSomething();
        hasBeenCalled = true;
    }
});

Now, everytime you scroll we first check if the function has been called before we do any work. Only if it hasn't do we check if the element is in view. After we call our function doSomething() we make sure to set hasBeenCalled to true so the function won't be triggered again. This is a good improvement, but I can't help but think that every time we scroll, we are still triggering an event, even if we don't do a lot of computation with it. Let's try to improve this by clearing the event after it has been triggered.

First, we will define the onScroll function so we can reference it later. When the event is triggered, we will remove the event from the page.

const onScroll = () => {
    const element = document.getElementById("my-element");
    if (!isElementInView(element)) {
        return;
    }
    doSomething();
    window.removeEventListener("scroll", onScroll);
};
window.addEventListener("scroll", onScroll);

Note, I've updated the condition so that if the element is not in view we exit the function with a return. This works great, but I can't help but think about how often the scroll event is triggered. If I slowly scroll the page by 200 pixels, that means I am calling the onScroll at least 200 times. As a consequence I'm also calling document.getElementById 200 times, and then I'm checking the bounding box properties 200 times as well. The more I scroll, the more I call these expensive DOM methods.

One trick we can use is to limit the number of times the function can be called in an interval. This method is called throttling. For example, we can say: only trigger the scroll event once every 200ms. This will drastically reduce the performance hit. We can do so by adding a delay whenever the event is triggered.

let isCheckingScroll = false;
const delay = 200; // milliseconds
const onScroll = () => {
    if (isCheckingScroll) {
        return ;
    }
    isCheckingScroll = true;
    setTimeout(() => {
        isCheckingScroll = false;
    }, delay);
    const element = document.getElementById("my-element");
    if (!isElementInView(element)) {
        return;
    }
    doSomething();
    window.removeEventListener("scroll", onScroll);
};
window.addEventListener("scroll", onScroll);

When the onScroll event is triggered, we check if isCheckingScroll has been set to true already. This prevents any further work. If it hasn't been set, we set isCheckingScroll preventing any further work. Then we create a time out timeout that will reset the value to false in 200 milliseconds in our case. This means, the function will only be called once every 200ms.

We've come a long way from where we started. Now we know how to efficiently trigger an event when an element is in view. But it becomes tedious to rewrite these steps any time we need to monitor an element. Especially when you want to have multiple elements you want to track on the page. So the next step will be to package this into a reusable function. Let's create a function called whenInView(). This function will take an element, a call back function, and rate for how often we want to check during scrolling.

const whenInView = (el, cb, rate = 200) => {
  let isCheckingScroll = false;
  const onScroll = () => {
    if (isCheckingScroll) {
      return;
    }
    isCheckingScroll = true;
    setTimeout(() => {
      isCheckingScroll = false;
    }, rate);
    if (!isElementInView(el)) {
      return;
    }
    window.removeEventListener("scroll", onScroll);
    cb();
  };
  window.addEventListener("scroll", onScroll);
};

Here is an example on how to use it:

const doSomething = () => {
    console.log("Open the box of chocolate");
};

const box = document.querySelector(".box-of-chocolate");
whenInView(box, doSomething, 200);

When the element is in view, we should see the message in the console. You can increase or decrease the rate to something that works best for you.

Other considerations.

One thing to take into account is the whenInView function does not consider if you want to trigger these events multiple times. For example, if the element goes out of view, and then back into view, do we want to trigger it again? I'll leave it as an exercise for the reader.

Also, the isElementInView function only triggers if the element is fully in view. Depending on your needs, you might want to implement a function that considers when the element is partially in view.

const isElementPartiallyInView = (el) => {
    if (!el) return false;
    const rect = el.getBoundingClientRect();
    const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
    const viewportWidth = window.innerWidth || document.documentElement.clientWidth;

    return (
        rect.bottom > 0 && // Element's bottom is below the top of the viewport
        rect.right > 0 &&  // Element's right is to the right of the left of the viewport
        rect.top < viewportHeight && // Element's top is above the bottom of the viewport
        rect.left < viewportWidth // Element's left is to the left of the right of the viewport
    );
};

Modern Solution.

Many years back, I had tackled this very issue but there were no solutions provided by the browser. But today, we have an API that takes away all the guess work and let the browser properly calculate when an element is in view saving us precious resources. We can use the IntersectionObserver API. Here is how it works:

const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            doSomething();
            observer.unobserve(entry.target); // Stop observing once in view
        }
    });
}, {
    root: null, // default is the viewport
    rootMargin: '0px', // no extra margin
    threshold: 0.1 // trigger when 10% of the element is visible
});

observer.observe(box);

So the observer observes the element we want to watch and when it is in view it will intersect with the and trigger the call back. Note that we can make use of the threshold parameter to decide how much of the element, in percentage, needs to be visible before we trigger our call back.

We can easily wrap this function as well in our whenInView method and if the browser supports it make use of it:

// Final code
const RATE = 200; // default rate for scrollInView
const scrollInView = (el, cb) => {
  let isCheckingScroll = false;
  const onScroll = () => {
    if (isCheckingScroll) {
      return;
    }
    isCheckingScroll = true;
    setTimeout(() => {
      isCheckingScroll = false;
    }, RATE);
    if (!isElementInView(el)) {
      return;
    }
    window.removeEventListener("scroll", onScroll);
    cb();
  };
  window.addEventListener("scroll", onScroll);
};

const isElementInView = (el) => {
  if (!el) return false;
  const rect = el.getBoundingClientRect();
  const viewportHeight =
    window.innerHeight || document.documentElement.clientHeight;
  const viewportWidth =
    window.innerWidth || document.documentElement.clientWidth;
  return (
    rect.top >= 0 &&
    rect.left >= 0 &&
    rect.bottom <= viewportHeight &&
    rect.right <= viewportWidth
  );
};

const observeInView = (element, callback) => {
  const observer = new IntersectionObserver(
    (entries, observer) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          callback();
          observer.unobserve(entry.target); // Stop observing once in view
        }
      });
    },
    {
      root: null, // default is the viewport
      rootMargin: "0px", // no extra margin
      threshold: 0.5, // 50% of the element must be visible
    }
  );

  observer.observe(element);
};

const whenInView = ( () => {
    return "IntersectionObserver" in window
        ? observeInView
        : scrollInView;
})();

// Example usage:
whenInView(document.querySelector('#myElement'), () => {
  console.log('Element is in view!');
});

Did you like this article? Subscribe for more and follow updates via RSS.

Back to JavaScript articles