Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Some higher level abstractions for detecting DOM element visibility [duplicate]

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

(The question refers to Firefox.)

like image 684
benzaita Avatar asked Sep 23 '08 21:09

benzaita


People also ask

How to set the visibility for an element in HTML DOM?

The Style visibility property in HTML DOM used to set the visibility for an element.It is used to hide or show the element. It returns the visibility property that is given to an element. Syntax: Property Values: visible: It is used to specify the element to be visible.It is a default value. hidden: Element is not visible but it affects layout.

How to check if an element is visible in Dom?

- GeeksforGeeks How to check if an element is visible in DOM ? Method 1: Checking the height, width and bounding rectangle of the element: The element can be checked whether it is visible by checking the height, width, and dimensions of the bounding rectangle of the element.

What is the use of style visibility property in HTML?

The Style visibility property in HTML DOM used to set the visibility for an element.It is used to hide or show the element. It returns the visibility property that is given to an element. Syntax: Property Values: visible: It is used to specify the element to be visible.It is a default value.

What is the difference between display none and visibility hidden?

However, the difference is that if you set display:none, it hides the entire element, while visibility:hidden means that the contents of the element will be invisible, but the element stays in its original position and size.


7 Answers

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.


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.
  • Try something new - The Intersection Observer API explained.
like image 180
31 revs, 11 users 78% Avatar answered Oct 12 '22 05:10

31 revs, 11 users 78%


We have now a native javascript Intersection Observer API from which we can detect elements either they are in the viewport or not.

Here is example

const el = document.querySelector('#el')
const observer = new window.IntersectionObserver(([entry]) => {
  if (entry.isIntersecting) {
    console.log('ENTER')
    return
  }
  console.log('LEAVE')
}, {
  root: null,
  threshold: 0.1, // set offset 0.1 means trigger if atleast 10% of element in viewport
})

observer.observe(el);
body {
  height: 300vh;
}

#el {
  margin-top: 100vh;
}
<div id="el">this is element</div>
like image 20
Ismail Farooq Avatar answered Oct 12 '22 06:10

Ismail Farooq


I tried Dan's answer, however, the algebra used to determine the bounds means that the element must be both ≤ the viewport size and completely inside the viewport to get true, easily leading to false negatives. If you want to determine whether an element is in the viewport at all, ryanve's answer is close but the element being tested should overlap the viewport, so try this:

function isElementInViewport(el) {
    var rect = el.getBoundingClientRect();

    return rect.bottom > 0 &&
        rect.right > 0 &&
        rect.left < (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */ &&
        rect.top < (window.innerHeight || document.documentElement.clientHeight) /* or $(window).height() */;
}
like image 30
Walf Avatar answered Oct 12 '22 05:10

Walf


Update: 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
  );
}
like image 26
Prestaul Avatar answered Oct 12 '22 04:10

Prestaul


The simplest solution as the support of Element.getBoundingClientRect() has become perfect:

function isInView(el) {
  const box = el.getBoundingClientRect();
  return box.top < window.innerHeight && box.bottom >= 0;
}
like image 43
leonheess Avatar answered Sep 29 '22 12:09

leonheess


Update

In modern browsers, you might want to check out the Intersection Observer API which provides the following benefits:

  • Better performance than listening for scroll events
  • Works in cross domain iframes
  • Can tell if an element is obstructing/intersecting another

Intersection Observer is on its way to being a full-fledged standard and is already supported in Chrome 51+, Edge 15+ and Firefox 55+ and is under development for Safari. There's also a polyfill available.


Previous answer

There are some issues with the answer provided by Dan that might make it an unsuitable approach for some situations. Some of these issues are pointed out in his answer near the bottom, that his code will give false positives for elements that are:

  • Hidden by another element in front of the one being tested
  • Outside the visible area of a parent or ancestor element
  • An element or its children hidden by using the CSS clip property

These limitations are demonstrated in the following results of a simple test:

Failed test, using isElementInViewport

The solution: isElementVisible()

Here's a solution to those problems, with the test result below and an explanation of some parts of the code.

function isElementVisible(el) {
    var rect     = el.getBoundingClientRect(),
        vWidth   = window.innerWidth || document.documentElement.clientWidth,
        vHeight  = window.innerHeight || document.documentElement.clientHeight,
        efp      = function (x, y) { return document.elementFromPoint(x, y) };     

    // Return false if it's not in the viewport
    if (rect.right < 0 || rect.bottom < 0 
            || rect.left > vWidth || rect.top > vHeight)
        return false;

    // Return true if any of its four corners are visible
    return (
          el.contains(efp(rect.left,  rect.top))
      ||  el.contains(efp(rect.right, rect.top))
      ||  el.contains(efp(rect.right, rect.bottom))
      ||  el.contains(efp(rect.left,  rect.bottom))
    );
}

Passing test: http://jsfiddle.net/AndyE/cAY8c/

And the result:

Passed test, using isElementVisible

Additional notes

This method is not without its own limitations, however. For instance, an element being tested with a lower z-index than another element at the same location would be identified as hidden even if the element in front doesn't actually hide any part of it. Still, this method has its uses in some cases that Dan's solution doesn't cover.

Both element.getBoundingClientRect() and document.elementFromPoint() are part of the CSSOM Working Draft specification and are supported in at least IE 6 and later and most desktop browsers for a long time (albeit, not perfectly). See Quirksmode on these functions for more information.

contains() is used to see if the element returned by document.elementFromPoint() is a child node of the element we're testing for visibility. It also returns true if the element returned is the same element. This just makes the check more robust. It's supported in all major browsers, Firefox 9.0 being the last of them to add it. For older Firefox support, check this answer's history.

If you want to test more points around the element for visibility―ie, to make sure the element isn't covered by more than, say, 50%―it wouldn't take much to adjust the last part of the answer. However, be aware that it would probably be very slow if you checked every pixel to make sure it was 100% visible.

like image 228
Andy E Avatar answered Oct 12 '22 04:10

Andy E


See the source of verge, which uses getBoundingClientRect. It's like:

function inViewport (element) {
  if (!element) return false;
  if (1 !== element.nodeType) return false;

  var html = document.documentElement;
  var rect = element.getBoundingClientRect();

  return !!rect &&
    rect.bottom >= 0 &&
    rect.right >= 0 && 
    rect.left <= html.clientWidth &&
    rect.top <= html.clientHeight;
}

It returns true if any part of the element is in the viewport.

like image 45
ryanve Avatar answered Oct 12 '22 06:10

ryanve