Frontend Development 8 min read

Encapsulating DOM Exposure: From Scroll Listener to IntersectionObserver

This article describes how to abstract and improve DOM exposure tracking by first using a scroll listener with getBoundingClientRect and then refactoring to a more robust IntersectionObserver implementation, including code examples, configuration details, and compatibility considerations.

政采云技术
政采云技术
政采云技术
Encapsulating DOM Exposure: From Scroll Listener to IntersectionObserver

With the increasing demand for exposure tracking, the author extracted previously written exposure logic into a reusable module.

Initial Version

The logic relies on window.scroll events and getBoundingClientRect() to determine if an element is within the viewport.

Key code:

function buryExposure (el, fn) {
  let elEnter = false; // dom是否进入可视区域
  el.exposure = () => {
    const { top } = el.getBoundingClientRect();
    if (top > 0 && top < window.screen.availHeight) {
      if (!elEnter) {
        elEnter = true;
        fn(el)
      }
    } else {
      elEnter = false;
    };
  }
  document.addEventListener('scroll', el.exposure);
}

Two important points:

Checking the top margin against the viewport to decide exposure.

Using a flag ( elEnter ) to avoid repeated callbacks while the element stays visible.

Rewrite with IntersectionObserver

When a requirement arose to track horizontal scrolling inside a container, the scroll‑based approach proved insufficient, and performance concerns about getBoundingClientRect were raised.

The author turned to window.IntersectionObserver , an asynchronous API that observes the intersection of a target element with an ancestor or the viewport.

Typical usage:

const io = new IntersectionObserver(callback, options)
io.observe(DOM)

Configuration options:

const observerOptions = {
  root: null,          // viewport as root
  rootMargin: '0px',    // no margin
  threshold: [...Array(100).keys()].map(x => x / 100) // trigger on every 0.01 change
}

Callback example:

(entries) => {
  entries.forEach(item => {
    if (item.intersectionRatio > 0 && item.intersectionRatio <= 1) {
      // partially visible → treat as exposure
    } else if (item.intersectionRatio === 0) {
      // not visible → reset
    }
  });
}

The observer reports intersectionRatio values, where 1 means fully visible, values between 0 and 1 indicate partial visibility, and 0 means hidden. The isIntersecting property can also be used.

However, the author found that in some real‑world scenarios (e.g., a horizontally scrolling banner), the observer could report an element as intersecting even when the user cannot actually see it because a parent container clips it.

Final Version

To ensure broader compatibility, a polyfill is loaded when IntersectionObserver is unavailable:

// Use W3C polyfill
require('intersection-observer');

The final encapsulated function:

/**
 * DOM exposure
 * @param {object} options 配置参数
 *   options.DOMs      - array of DOM elements to observe
 *   options.callback  - function to call on exposure
 *   options.parentDom - optional root element for observation
 */
export default function expose (options = {}) {
  if (!options.DOMs || !options.callback) {
    console.error('Error: 传入监听DOM或者回调函数');
    return;
  }
  const observerOptions = {
    root: null,
    rootMargin: '0px',
    threshold: [...Array(100).keys()].map(x => x / 100)
  };
  options.parentDom && (observerOptions.root = options.parentDom);
  if (window.IntersectionObserver) {
    let elEnter = false; // dom是否进入可视区域
    const io = new IntersectionObserver((entries) => {
      const fn = () => options.callback({ io });
      entries.forEach(item => {
        if (!elEnter && item.intersectionRatio > 0 && item.intersectionRatio <= 1) {
          fn();
          elEnter = true;
        } else if (item.intersectionRatio === 0) {
          elEnter = false;
        }
      });
    }, observerOptions);
    options.DOMs.forEach(DOM => io.observe(DOM));
  }
}

References

Further reading on getBoundingClientRect and IntersectionObserver can be found at the linked articles in the original post.

frontendJavaScriptIntersectionObserverDOMexposure
政采云技术
Written by

政采云技术

ZCY Technology Team (Zero), based in Hangzhou, is a growth-oriented team passionate about technology and craftsmanship. With around 500 members, we are building comprehensive engineering, project management, and talent development systems. We are committed to innovation and creating a cloud service ecosystem for government and enterprise procurement. We look forward to your joining us.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.