albert.walickihire me
← all solutions
javascript

How to check if an element is visible in the viewport?

Detect when an element scrolls into view with IntersectionObserver, do a one-off check with getBoundingClientRect, and detect CSS-hidden elements.

Use IntersectionObserver. It calls your function when an element enters or leaves the viewport, so you don't need a scroll listener. This example adds a class the first time each .reveal element comes into view:

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) return;

      entry.target.classList.add('is-visible');
      observer.unobserve(entry.target); // reveal only once
    });
  },
  { threshold: 0.2 }
);

document.querySelectorAll('.reveal').forEach((el) => {
  observer.observe(el);
});

The callback also runs once right after observe(), with the element's current state, so elements that are already on screen get the class immediately.

The options control when it fires:

  • threshold is how much of the element has to be visible, from 0 to 1. The default 0 fires as soon as a single pixel is in view, 0.2 at 20%, and 1 only when the whole element is visible. An array like [0, 0.5, 1] fires at each step.
  • rootMargin grows or shrinks the area that counts as the viewport, using CSS margin syntax. rootMargin: '200px 0px' fires 200px before the element scrolls into view, which is what you want for loading images or the next page of results early. A negative value like '-10% 0px' fires only once the element is further inside.
  • unobserve() stops watching one element. Use it for one-time effects. To keep tracking, for example to pause something when it leaves the screen, skip it and use entry.isIntersecting both ways.

If you use this for reveal animations, make sure the content is still visible when JavaScript fails, and skip the animation for people who prefer reduced motion.

A one-off check with getBoundingClientRect

When you need an answer right now, say in a click handler, compare the element's rectangle with the window size. There are two different questions: is the element fully visible, or is any part of it visible?

function isFullyInViewport(el) {
  const rect = el.getBoundingClientRect();
  return (
    rect.top >= 0 &&
    rect.left >= 0 &&
    rect.bottom <= window.innerHeight &&
    rect.right <= window.innerWidth
  );
}

function isPartlyInViewport(el) {
  const rect = el.getBoundingClientRect();
  return (
    rect.top < window.innerHeight &&
    rect.bottom > 0 &&
    rect.left < window.innerWidth &&
    rect.right > 0
  );
}

For a 200px tall element 700px from the top of an 800px tall window, isFullyInViewport returns false and isPartlyInViewport returns true. More about what those rectangle values mean is in how to get the position of an element.

Don't call these on every scroll event. Scroll events fire constantly while the page moves, and getBoundingClientRect() makes the browser recalculate layout whenever something has changed since the last read. With many elements, that's a classic cause of janky scrolling. IntersectionObserver lets the browser do the math as part of its normal rendering work and only calls you when the answer changes.

In the viewport isn't the same as visible

Both approaches only look at geometry. An element with visibility: hidden or opacity: 0 can be "in the viewport" while nobody can see it. An element with display: none has a rectangle of all zeros, which isFullyInViewport counts as fully visible.

To check whether CSS hides it, use checkVisibility():

el.checkVisibility({ opacityProperty: true, visibilityProperty: true });

It returns false when the element or one of its ancestors has display: none, and the options add checks for opacity: 0 and visibility: hidden. It's fairly new, so check Can I Use for support. It still doesn't tell you whether the element is scrolled into view or covered by another element, so combine it with one of the checks above when you need both.

more solutions
work with me

Got something that needs building?

Frontend builds, full-stack features in Django, design-system work. Available for work.

See my workGet in touch