Pooja in Web Designing

Is there an efficient way to tell if a DOM element (in an HTML document) is currently visible (appears in the viewport)?

3 Answers

0 votes
Nadira

Time marches on and so have our browsers. This technique is no longer recommended and you should use Dan's solution if you do not need to support version of Internet Explorer before 7.

Original solution (now outdated):

This will check if the element is entirely visible in the current viewport:

function elementInViewport(el) {
  var top = el.offsetTop;
  var left = el.offsetLeft;
  var width = el.offsetWidth;
  var height = el.offsetHeight;

  while(el.offsetParent) {
    el = el.offsetParent;
    top += el.offsetTop;
    left += el.offsetLeft;
  }

  return (
    top >= window.pageYOffset &&
    left >= window.pageXOffset &&
    (top + height) <= (window.pageYOffset + window.innerHeight) &&
    (left + width) <= (window.pageXOffset + window.innerWidth)
  );
}

You could modify this simply to determine if any part of the element is visible in the viewport:

function elementInViewport2(el) {
  var top = el.offsetTop;
  var left = el.offsetLeft;
  var width = el.offsetWidth;
  var height = el.offsetHeight;

  while(el.offsetParent) {
    el = el.offsetParent;
    top += el.offsetTop;
    left += el.offsetLeft;
  }

  return (
    top < (window.pageYOffset + window.innerHeight) &&
    left < (window.pageXOffset + window.innerWidth) &&
    (top + height) > window.pageYOffset &&
    (left + width) > window.pageXOffset
  );
}
0 votes
Nadira

Now most browsers support getBoundingClientRect method, which has become the best practice. Using an old answer is very slow, not accurate and has several bugs.

The solution selected as correct is almost never precise. You can read more about its bugs.


This solution was tested on Internet Explorer 7 (and later), iOS 5 (and later) Safari, Android 2.0 (Eclair) and later, BlackBerry, Opera Mobile, and Internet Explorer Mobile 9.


function isElementInViewport (el) {
 // Special bonus for those using jQuery
 if (typeof jQuery === "function" && el instanceof jQuery) {
 el = el[0];
 }
 var rect = el.getBoundingClientRect();
 return (
 rect.top >= 0 &&
 rect.left >= 0 &&
 rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /* or $(window).height() */
 rect.right <= (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */
 );
}

How to use:

You can be sure that the function given above returns correct answer at the moment of time when it is called, but what about tracking element's visibility as an event?

Place the following code at the bottom of your <body> tag:

function onVisibilityChange(el, callback) {
 var old_visible;
 return function () {
 var visible = isElementInViewport(el);
 if (visible != old_visible) {
 old_visible = visible;
 if (typeof callback == 'function') {
 callback();
 }
 }
 }
}
var handler = onVisibilityChange(el, function() {
 /* Your code go here */
});
// jQuery
$(window).on('DOMContentLoaded load resize scroll', handler);
/* // Non-jQuery
if (window.addEventListener) {
 addEventListener('DOMContentLoaded', handler, false);
 addEventListener('load', handler, false);
 addEventListener('scroll', handler, false);
 addEventListener('resize', handler, false);
} else if (window.attachEvent) {
 attachEvent('onDOMContentLoaded', handler); // Internet Explorer 9+ :(
 attachEvent('onload', handler);
 attachEvent('onscroll', handler);
 attachEvent('onresize', handler);
}
*/

If you do any DOM modifications, they can change your element's visibility of course.

Guidelines and common pitfalls:

Maybe you need to track page zoom / mobile device pinch? jQuery should handle zoom/pinch cross browser, otherwise first or second link should help you.

If you modify DOM, it can affect the element's visibility. You should take control over that and call handler() manually. Unfortunately, we don't have any cross browser onrepaint event. On the other hand that allows us to make optimizations and perform re-check only on DOM modifications that can change an element's visibility.

Never Ever use it inside jQuery $(document).ready() only, because there is no warranty CSS has been applied in this moment. Your code can work locally with your CSS on a hard drive, but once put on a remote server it will fail.

After DOMContentLoaded is fired, styles are applied, but the images are not loaded yet. So, we should add window.onload event listener.

We can't catch zoom/pinch event yet.

The last resort could be the following code:

/* TODO: this looks like a very bad code */
setInterval(handler, 600);

You can use the awesome feature pageVisibiliy of the HTML5 API if you care if the tab with your web page is active and visible.

TODO: this method does not handle two situations:

  • overlapping using z-index
  • using overflow-scroll in element's container
0 votes
Nadira

All answers I've encountered here only check if the element is positioned inside the current viewport. But that doesn't mean that it is visible.
What if the given element is inside a div with overflowing content, and it is scrolled out of view?

To solve that, you'd have to check if the element is contained by all parents.
My solution does exactly that:

It also allows you to specify how much of the element has to be visible.

Element.prototype.isVisible = function(percentX, percentY){
 var tolerance = 0.01; //needed because the rects returned by getBoundingClientRect provide the position up to 10 decimals
 if(percentX == null){
 percentX = 100;
 }
 if(percentY == null){
 percentY = 100;
 }
 var elementRect = this.getBoundingClientRect();
 var parentRects = [];
 var element = this;
 while(element.parentElement != null){
 parentRects.push(element.parentElement.getBoundingClientRect());
 element = element.parentElement;
 }
 var visibleInAllParents = parentRects.every(function(parentRect){
 var visiblePixelX = Math.min(elementRect.right, parentRect.right) - Math.max(elementRect.left, parentRect.left);
 var visiblePixelY = Math.min(elementRect.bottom, parentRect.bottom) - Math.max(elementRect.top, parentRect.top);
 var visiblePercentageX = visiblePixelX / elementRect.width * 100;
 var visiblePercentageY = visiblePixelY / elementRect.height * 100;
 return visiblePercentageX + tolerance > percentX && visiblePercentageY + tolerance > percentY;
 });
 return visibleInAllParents;
};

This solution ignored the fact that elements may not be visible due to other facts, like opacity: 0.

I have tested this solution in Chrome and Internet Explorer 11.

Related questions

Category

Follow Us

Stay updated via social channels

Twitter Instagram LinkedIn Instagram
...