How can I tell if a DOM element is visible in the current viewport?
Asked Answered
M

31

1269

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.)

Midday answered 23/9, 2008 at 21:24 Comment(8)
Depends what you mean by visible. If you mean is it currently shown on the page, given the scroll position, you can calculate it based on the elements y offset and the current scroll position.Kmeson
I've added my own solution that solves this problemTwotone
Do any of these solutions take into account the z-index of a dom node and how that might affect visibility specifically by possibly hiding elements with a lower z-index?Evalynevan
None of the answers provided work with generated absolute positioned elements.Stoma
A 2019 answer: IntersectionObserverCopperhead
There are one million answers and most are ridiculously long. See here for a two-linerSkeens
Use Intersection Observer API to Detect if Element is in Viewport, details here – frontendguruji.com/blog/…Overslaugh
In addition to all of the Intersection Observer answers, this visualizer is great for configuring the variables for Intersection Observer: codepen.io/michellebarker/pen/xxwLpRGNata
V
422

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
  );
}
Verein answered 24/9, 2008 at 2:40 Comment(11)
Original function posted had a mistake. Needed to save the width/height before reassigning el...Verein
It also might be wise to abstract this a bit and create some utility functions. I use one called getViewport that returns the top/left/bottom/right of the visible window, and one called getPosition that finds the top/left of an element.Verein
I'm trying to get this functionality in a Firefox extension, so just in case someone else is in the same situation... (Web developers, you can of course ignore this comment entirely) This function works perfectly in that context, EXCEPT that window refers to the wrong object, so just put var window = el.ownerDocument.defaultView at the top of the function and you're good to go.Coons
What if the element lives in a scrollable div and scrolled out of a view??Lomond
Please review a newer version of the script belowTelic
In a Firefox extension, el.ownerDocument.defaultView.innerWidth is not an accurate representation of the viewport (though neither is window). For example, it will be wrong if you make your firefox window narrower than the widest installed toolbar.Terza
Also curious about @amartynov's question. Anyone know how to simply tell if an element is hidden due to overflow of an ancestor element? Bonus if this can be detected regardless of how deeply nested the child is.Knitting
i'm wonder why you recommend below answer while it's pure javascript, and does not seem to use any thing unsupportedOlney
@deadManN recursing through the DOM is notoriously slow. That is reason enough, but the browser vendors have also created getBoundingClientRect for specifically the purpose of finding element coordinates... Why wouldn't we use it?Verein
The function works fine if there are not blocks with scrollbars. I suggest to modify while block and the final check: while(elm.offsetParent) {elm = elm.offsetParent; top += elm.offsetTop - elm.scrollTop; left += elm.offsetLeft - elm.scrollLeft; } return (top < (window.innerHeight) && left < (window.innerWidth) && (top + height) > 0 && (left + width) > 0 );Zemstvo
I suggest adding developer.mozilla.org/en-US/docs/Web/API/… although it doesn't support IE11, so you have to use fallback method.Interphone
T
1700

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:

Telic answered 23/9, 2008 at 21:25 Comment(44)
I'm using this solution (beware the "botom" typo, though). There is also something to be aware of, when the element we're considering would have images into it. Chrome (at least) must wait for the image to be loaded to have the exact value for the boundingRectangle. Seems that Firefox does not have this "problem"Exordium
window.innerHeight and innerWidth are undefined in IE. There's a good article at howtocreate.co.uk/tutorials/javascript/browserwindow showing the various ways to get the document height...document.body.clientHeight seems to be the best way overall. I've edited your answer accordingly.Diaz
here's my version of the check, it just checks if the element is visible in vp at all: gist.github.com/2300296Undertone
If you don't need to support IE 5.5 and lower (which you probably don't), you can use (window.innerWidth || document.documentElement.clientWidth) and the height equivalents for a cross browser solution.Twotone
@Claudio: the problem is that page is REPAINTED after each picture is loaded. So, if you call my function before the picture comes, you get a correct result, but it's valid for a few milliseconds until the picture apears and the container gets extendedTelic
@MatasPetrikas your version has mistakes, but you've been fixed in commentsTelic
Why that space after document. , in "rect.bottom <= (window.innerHeight || document. documentElement.clientHeight)" ??Arruda
Does it work when you have scrolling enabled in a container inside body. For e.g it doesn't work here - agaase.github.io/webpages/demo/isonscreen2.html isElementInViewport(document.getElementById("innerele")). innerele is present inside a container which has scrolling enabled.Squier
Do I need to use all these DOMContentLoaded load resize scroll? Any performance issue etc!Winola
I see it is not working on other element jQuery(window).on than on window but I see handler is called MANY times on every scroll. Is there a way to tie the event on particular element?Winola
The calculations assume that the element is smaller than the screen. If you have high or wide elements, it might be more accurate to use return (rect.bottom >= 0 && rect.right >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight) && rect.left <= (window.innerWidth || document.documentElement.clientWidth));Cephalopod
Tip: To those trying to implement this with jQuery, just a friendly reminder to pass in the HTML DOM object (e.g., isElementInViewport(document.getElementById('elem'))) and not the jQuery object (e.g., isElementInViewport($("#elem))). The jQuery equivalent is to add [0] like so: isElementInViewport($("#elem)[0]).Hindermost
#996414Telic
Brilliant. If not using jQuery, instanceof jQuery throws an error.Ozuna
@Dan—if (jQuery != undefined) doesn't fix anything, it will still throw a reference error if jQuery hasn't been defined. The way to fix it (i.e. prevent an error if jQuery hasn't been defined or isn't a constructor) is if (typeof jQuery == 'function' && el.instanceof jQuery), which isn't bullet proof (because instanceof isn't particularly useful) but should be good enough.Ozuna
You can catch the zoom/pinch event. The resize event is fired when either the window is resized or the page is zoomed.Ozone
Here's my version, supports container elements (e.g. if the element is within another scrollable element) and total or partial visibility (jQuery only though): gist.github.com/anonymous/f09b5cd487b5ad55c46eTuscany
@Tuscany — I like your solution, it looks like exactly what I need. Can you give an example of usage? I'm not clear on how to reference that type of function.Inhibitory
@ToddProuty once declared, you can do $("#elementInside").isInViewport("#parentElement") (returns true if #elementInside is fully visible within #parentElement) or $("#elementInside").isInViewport("#parentElement", true) (returns true if #elementInside is at least partially visible within #parentElement) or $("#elementInside").isInViewport() (returns true if visible within the main window)Tuscany
This approach fails on overflowing elements which are in current viewport but invisible.Sputter
At least in the developer tools, in Firefox, getBoundingClientRect() is giving me values just slightly wider than the viewport for elements with width:100%, i.e. the viewport is 815px and the value of "right" is 815.2000122070312. I used Math.floor() on the properties of rect to work around this.Flair
How would I use this to get all the visible elements in the current viewport? For bonus I would have the coords relative to the viewport.Orgy
I would like to check for multiple jQuery elements that can be in (or out of) the viewport. This script seems to check for one element only. When multiple elements are in the current viewport, I want to retrieve the last element that's in the current viewport. How could this be done using this script? Or some kind of modification?Rogan
@Orgy I created a Gist which returns the lowest element in your current viewport. If you just return the visibleElements array in this function you have exactly what you need. See: gist.github.com/vernondegoede/5b5638706aec5a80611010c88609d170Rogan
What if you have a collection of elements?Guilford
The algebra for this needs clarifying. It's more like testing whether an element is completely outside the viewport. It returns 'invisible' for elements which are only partially outside the viewport. See @Walf answer for more intuitive version. (of isElementInViewport). The event listening works great.Hum
el is not definedTier
Found this complement function useful for determining when to scroll when it is not visible: function alignToTop(el) { let rect = el.getBoundingClientRect(); return rect.top < 0 || rect.bottom <= (window.innerHeight || document.documentElement.clientHeight); } Spine
I have found this to work nearly perfectly, except for extremely fast scrolling, e.g. pressing cmd down or cmd up it can sometimes not be accurate. I have found the alternative is to use github.com/stutrek/scrollMonitor which is always 100% accurate for me, and very performantTalipes
This has a huge limitation, though. Imagine an SVG node, even as simple as a rectangle, that is rotated, say 45 degrees counterclockwise. Its (original) "left" border, now inclinated 45 degrees, can easily be beyond the top-right corner of the viewport, hance having the whole rectangle outside of the viewport, and yet the bounding box will still overlap it.Startling
const onVisibilityChange = (el, callback) => () => isElementInViewport(el) && callback()Whangee
This answer checks if an element is entirely displayed in the viewport. If part of it is cut-off, this will treat it as not displayed. If you want to know is any part of the element is visible, use the snippet from @Roonaan's commentStylet
Hey, I think it might be worth to add if (!el) { return; } to avoid crashes, just in case.Cerumen
This code only works with single element, you can't attach this event to eg. whole css class. I know one could do a loop through all the class elements and attach the event to them, but then what about dynamically added elements?Medlin
It should be noted that getBoundingClientRect can be EXTREMELY slow, in complex pages it can take several hundred milliseconds (!) to complete, so if you know exactly how your page is layed out and how the elements are nested, it might be better to use clientLeft / clientTop etc.Pluvious
no luck for me..it return true even element is not in the viewportNonrigid
This approach works great for me. I just need one modification. I want to determine if 50% of the element is in the viewport or not! Right now it immediately tells me even if just the top of the element has been scrolled into the viewport.Phosphene
Uncaught ReferenceError: el is not definedDysplasia
@Roonaan, your comment just solved my issue. Missed that point earlier and was getting getBoundingClientRect is not a function.Iliac
IntersectionObserver is currently well supported and is IMO the correct way of doing this. See https://mcmap.net/q/33007/-how-can-i-tell-if-a-dom-element-is-visible-in-the-current-viewport for an example.Nell
According to https://mcmap.net/q/33025/-getboundingclientrect-is-returning-zero-in-xul getBoundingClientRect() will return 0s if an element has display: none; so I find myself adding && (rect.bottom || rect.right || rect.top || rect.left) to the end of the return statement.Cutworm
The initial value of old_visible should be false, right now it's undefined and undefined is not the same as false.Spout
I just want to know, why people answer using JqueryBonsai
var old_visible; should be var old_visible = false; because false == null is false and false == undefined is also falseDonohue
V
422

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
  );
}
Verein answered 24/9, 2008 at 2:40 Comment(11)
Original function posted had a mistake. Needed to save the width/height before reassigning el...Verein
It also might be wise to abstract this a bit and create some utility functions. I use one called getViewport that returns the top/left/bottom/right of the visible window, and one called getPosition that finds the top/left of an element.Verein
I'm trying to get this functionality in a Firefox extension, so just in case someone else is in the same situation... (Web developers, you can of course ignore this comment entirely) This function works perfectly in that context, EXCEPT that window refers to the wrong object, so just put var window = el.ownerDocument.defaultView at the top of the function and you're good to go.Coons
What if the element lives in a scrollable div and scrolled out of a view??Lomond
Please review a newer version of the script belowTelic
In a Firefox extension, el.ownerDocument.defaultView.innerWidth is not an accurate representation of the viewport (though neither is window). For example, it will be wrong if you make your firefox window narrower than the widest installed toolbar.Terza
Also curious about @amartynov's question. Anyone know how to simply tell if an element is hidden due to overflow of an ancestor element? Bonus if this can be detected regardless of how deeply nested the child is.Knitting
i'm wonder why you recommend below answer while it's pure javascript, and does not seem to use any thing unsupportedOlney
@deadManN recursing through the DOM is notoriously slow. That is reason enough, but the browser vendors have also created getBoundingClientRect for specifically the purpose of finding element coordinates... Why wouldn't we use it?Verein
The function works fine if there are not blocks with scrollbars. I suggest to modify while block and the final check: while(elm.offsetParent) {elm = elm.offsetParent; top += elm.offsetTop - elm.scrollTop; left += elm.offsetLeft - elm.scrollLeft; } return (top < (window.innerHeight) && left < (window.innerWidth) && (top + height) > 0 && (left + width) > 0 );Zemstvo
I suggest adding developer.mozilla.org/en-US/docs/Web/API/… although it doesn't support IE11, so you have to use fallback method.Interphone
T
240

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.

Twotone answered 4/3, 2013 at 14:18 Comment(20)
Did you mean to use doc.documentElement.clientWidth? Should that be 'document.documentElement' instead? On a different note, this is the only method the also works for use cases like hiding the content of an element for accessibility using the CSS 'clip' property: snook.ca/archives/html_and_css/hiding-content-for-accessibilityUpgrade
@klamping: good catch, thanks. I'd copied it right out of my code where I was using doc as an alias for document. Yeah, I like to think of this as a decent solution for the edge cases.Twotone
Do you have any ideas of fixing z-index problem?Telic
@Christoph: although he didn't really make it clear, I assume Dan is talking about the fact that an element may be in front of the one you're checking for, but not hiding (ie, a transparent layer). For this scenario, it's obviously quite difficult to determine whether an element is visible or not, because my solution would give false negatives. It could be modified to return a non-boolean flag, ie. "hidden", "visible" or "indeterminate", however.Twotone
I don't find this working with hide/slideup. When the element has been slided up / hided, the function will not return true.Hillis
There's a bug. I rant the fiddle. The results of the tests are Test 1: false and Test 2: true. If I force the algorithm to use compareDocumentPosition by editing the line where contains is set so that it is set to compareDocumentPosition, then the results are Test 1: false, Test 2: false. That's because 0x10 should be bitwise-anded with the result of compareDocumentPosition. Or 0x14 could be used for equality since the specs say a contained node is always following.Topgallant
For me it is not working. But inViewport() in previous answer is working in FF.Winola
It may also be beneficial to check that the center of the element is visible if you have rounded corners or a transform applied, as the bounding corners may not return the expected element: element.contains(efp(rect.right - (rect.width / 2), rect.bottom - (rect.height / 2)))Liveryman
The declaration of efp would be better as efp = document.elementFromPoint.bind(document).Wickman
@schism, only if you don't care about IE8 support (and the fact that bound functions aren't well optimised in some browsers) :-)Twotone
@AndyE I guess I'm just privileged to not have to care about it! :-) Incidentally, this seems to work in 4.4 Webview but not Chrome...Wickman
Your solution are more elegant than the most upvoted answer. However, just like the above one, it fails on overflowing elements which are in current viewport but invisible.Sputter
I have an odd situation, but I want to note it for future readers. My project is embedded in another via an iframe. The iframe has a scrollbar, because the space allocated to it is larger than is made visible. Neither Dan nor Andy have an approach that works for my situation. Elements off the bottom of the iframe, that you would have to scroll to, count as visible via both approaches. rainyjune, ryanve and ally's answers also gives this false positive. There is no way I have yet found to discover the visible size of the iframe, only the allocated size.Flak
Did not work on inputs for me (chrome canary 50). Not sure why, maybe native rounder corners ? I had to reduce the coords slightly to make it work el.contains(efp(rect.left+1, rect.top+1)) || el.contains(efp(rect.right-1, rect.top+1)) || el.contains(efp(rect.right-1, rect.bottom-1)) || el.contains(efp(rect.left+1, rect.bottom-1))Funky
@rayjax it's because the chrome devs decided to change the behaviour of window.innerHeight/Width and break the web, see code.google.com/p/chromium/issues/detail?id=571297 and feel free to add your complaint to the rest!Twotone
@AndyE if I am correct, innerHeight/Width would impact only the first part of the function( is inside window) not the second (is visible) ?Funky
@AndyE This wouldn't always work for elements that are wider/taller than the viewport, because all corners could be outside the screen even though it's visibleLapillus
@AndyE to solve that you could test the corners of the intersection of the element's bounding client rect and the viewport rectLapillus
Also elementFromPoint can still return elements with opacity: 0Lapillus
Thanks for recommending the Intersection Observer APIMusty
G
110

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>
Grandpa answered 5/7, 2021 at 21:49 Comment(6)
This should be selected as the new correct answer.Lithophyte
Brilliant answerDorrisdorry
Best solution ive ever used for those still interested here it is set up to add remove class const observer = new window.IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { entry.target.classList.add("viewport__active"); return; } entry.target.classList.remove("viewport__active"); }, { root: null, threshold: 0.4 // 0.0 - 1.0 } );Bargainbasement
Best answer of the questionSelwyn
Easy and simple answerEaglet
Thought elegant, this is a very limited solution. You can't use it if you need synchronous check.Pitchblack
T
94

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() */;
}
Toad answered 29/4, 2013 at 2:36 Comment(0)
R
51

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.

Rosyrot answered 14/9, 2012 at 5:49 Comment(0)
R
42

As a public service:
Dan's answer with the correct calculations (element can be > window, especially on mobile phone screens), and correct jQuery testing, as well as adding isElementPartiallyInViewport:

By the way, the difference between window.innerWidth and document.documentElement.clientWidth is that clientWidth/clientHeight doesn't include the scrollbar, while window.innerWidth/Height does.

function isElementPartiallyInViewport(el)
{
    // Special bonus for those using jQuery
    if (typeof jQuery !== 'undefined' && el instanceof jQuery) 
        el = el[0];

    var rect = el.getBoundingClientRect();
    // DOMRect { x: 8, y: 8, width: 100, height: 100, top: 8, right: 108, bottom: 108, left: 8 }
    var windowHeight = (window.innerHeight || document.documentElement.clientHeight);
    var windowWidth = (window.innerWidth || document.documentElement.clientWidth);

    // https://mcmap.net/q/33026/-determine-whether-two-date-ranges-overlap
    var vertInView = (rect.top <= windowHeight) && ((rect.top + rect.height) >= 0);
    var horInView = (rect.left <= windowWidth) && ((rect.left + rect.width) >= 0);

    return (vertInView && horInView);
}


// https://mcmap.net/q/33007/-how-can-i-tell-if-a-dom-element-is-visible-in-the-current-viewport
function isElementInViewport (el)
{
    // Special bonus for those using jQuery
    if (typeof jQuery !== 'undefined' && el instanceof jQuery) 
        el = el[0];

    var rect = el.getBoundingClientRect();
    var windowHeight = (window.innerHeight || document.documentElement.clientHeight);
    var windowWidth = (window.innerWidth || document.documentElement.clientWidth);

    return (
           (rect.left >= 0)
        && (rect.top >= 0)
        && ((rect.left + rect.width) <= windowWidth)
        && ((rect.top + rect.height) <= windowHeight)
    );
}


function fnIsVis(ele)
{
    var inVpFull = isElementInViewport(ele);
    var inVpPartial = isElementPartiallyInViewport(ele);
    console.clear();
    console.log("Fully in viewport: " + inVpFull);
    console.log("Partially in viewport: " + inVpPartial);
}

Test-case

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Test</title>
    <!--
    <script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
    <script src="scrollMonitor.js"></script>
    -->

    <script type="text/javascript">

        function isElementPartiallyInViewport(el)
        {
            // Special bonus for those using jQuery
            if (typeof jQuery !== 'undefined' && el instanceof jQuery) 
                el = el[0];

            var rect = el.getBoundingClientRect();
            // DOMRect { x: 8, y: 8, width: 100, height: 100, top: 8, right: 108, bottom: 108, left: 8 }
            var windowHeight = (window.innerHeight || document.documentElement.clientHeight);
            var windowWidth = (window.innerWidth || document.documentElement.clientWidth);

            // https://mcmap.net/q/33026/-determine-whether-two-date-ranges-overlap
            var vertInView = (rect.top <= windowHeight) && ((rect.top + rect.height) >= 0);
            var horInView = (rect.left <= windowWidth) && ((rect.left + rect.width) >= 0);

            return (vertInView && horInView);
        }


        // https://mcmap.net/q/33007/-how-can-i-tell-if-a-dom-element-is-visible-in-the-current-viewport
        function isElementInViewport (el)
        {
            // Special bonus for those using jQuery
            if (typeof jQuery !== 'undefined' && el instanceof jQuery) 
                el = el[0];

            var rect = el.getBoundingClientRect();
            var windowHeight = (window.innerHeight || document.documentElement.clientHeight);
            var windowWidth = (window.innerWidth || document.documentElement.clientWidth);

            return (
                   (rect.left >= 0)
                && (rect.top >= 0)
                && ((rect.left + rect.width) <= windowWidth)
                && ((rect.top + rect.height) <= windowHeight)
            );
        }


        function fnIsVis(ele)
        {
            var inVpFull = isElementInViewport(ele);
            var inVpPartial = isElementPartiallyInViewport(ele);
            console.clear();
            console.log("Fully in viewport: " + inVpFull);
            console.log("Partially in viewport: " + inVpPartial);
        }


        // var scrollLeft = (window.pageXOffset !== undefined) ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body).scrollLeft,
        // var scrollTop = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;
    </script>
</head>

<body>
    <div style="display: block; width: 2000px; height: 10000px; background-color: green;">

        <br /><br /><br /><br /><br /><br />
        <br /><br /><br /><br /><br /><br />
        <br /><br /><br /><br /><br /><br />

        <input type="button" onclick="fnIsVis(document.getElementById('myele'));" value="det" />

        <br /><br /><br /><br /><br /><br />
        <br /><br /><br /><br /><br /><br />
        <br /><br /><br /><br /><br /><br />

        <div style="background-color: crimson; display: inline-block; width: 800px; height: 500px;" ></div>
        <div id="myele" onclick="fnIsVis(this);" style="display: inline-block; width: 100px; height: 100px; background-color: hotpink;">
        t
        </div>

        <br /><br /><br /><br /><br /><br />
        <br /><br /><br /><br /><br /><br />
        <br /><br /><br /><br /><br /><br />

        <input type="button" onclick="fnIsVis(document.getElementById('myele'));" value="det" />
    </div>

    <!--
    <script type="text/javascript">

        var element = document.getElementById("myele");
        var watcher = scrollMonitor.create(element);

        watcher.lock();

        watcher.stateChange(function() {
            console.log("state changed");
            // $(element).toggleClass('fixed', this.isAboveViewport)
        });
    </script>
    -->
</body>
</html>
Rosenstein answered 25/9, 2014 at 12:54 Comment(11)
isElementPartiallyInViewport is very useful as well. Nice one.Flair
For isElementInViewport(e) : Your code is not even loading images, This one is correct : function isElementInViewport(e) { var t = e.getBoundingClientRect(); return t.top >= 0 && t.left >= 0 && t.bottom <= (window.innerHeight || document.documentElement.clientHeight) && t.right <= (window.innerWidth || document.documentElement.clientWidth) }Nonfulfillment
@Arun chauhan: None of my code is loading images, so why should it, and the formula is correct.Rosenstein
@StefanSteiger Many answers here contain window.innerHeight || document.documentElement.clientHeight as part of the check. Since you're the first to refer to the difference between them, maybe you can also explain why even bother with this fallback? AFAIK window.innerHeight will always have a value (can't be null or undefined) so the only falsy value it can hold is 0. Which seems like a real edge case in which I'm not even sure how document.documentElement.clientWidth is suddenly helpful.Falda
@targumon: The reason is support of old browsers.Rosenstein
@StefanSteiger according to MDN it's supported since IE9 so it's practically safe (at least in my case) to just use window.innerHeight directly. Thanks!Falda
@targumon: Yea, but that means not in IE8.Rosenstein
@StefanSteiger I hope you don't have to support IE8 anymore. It's not supported anymore by Microsoft since January 2016!Scurrile
isElementPartiallyInViewport really helped me. thank you!Spillar
Your answer was used by "DiglettPotato" in Ebay's code fyi. he did give you creditDarell
@MCCCS: Haha, nice ! Glad it was of use to someone ;)Rosenstein
F
33

My shorter and faster version:

function isElementOutViewport(el){
    var rect = el.getBoundingClientRect();
    return rect.bottom < 0 || rect.right < 0 || rect.left > window.innerWidth || rect.top > window.innerHeight;
}

And a jsFiddle as required: https://jsfiddle.net/on1g619L/1/

Fluxion answered 23/4, 2014 at 3:4 Comment(3)
My solution are more greedy and faster, when element have any pixel in viewport , it's will return false.Fluxion
I like it. Concise. You could remove the spaces between function name and parenthesis, and between parenthesis and brace, on first line. Never liked those spaces. Maybe it's just my Text editor that color codes it all that still makes it easy to read. function aaa(arg){statements} I know that doesn't make it execute faster, falls under minifying instead.Siegler
This is literally the opposite of what the question asked, why is it allowed to exist as a solution? At the very minimum, the answer should state that this function can be used to check the element is outside of the viewport, not just rely on the function name to imply that.Tedium
N
28

The new Intersection Observer API addresses this question very directly.

This solution will need a polyfill as Safari, Opera and Internet Explorer don't support this yet (the polyfill is included in the solution).

In this solution, there is a box out of view that is the target (observed). When it comes into view, the button at the top in the header is hidden. It is shown once the box leaves the view.

const buttonToHide = document.querySelector('button');

const hideWhenBoxInView = new IntersectionObserver((entries) => {
  if (entries[0].intersectionRatio <= 0) { // If not in view
    buttonToHide.style.display = "inherit";
  } else {
    buttonToHide.style.display = "none";
  }
});

hideWhenBoxInView.observe(document.getElementById('box'));
header {
  position: fixed;
  top: 0;
  width: 100vw;
  height: 30px;
  background-color: lightgreen;
}

.wrapper {
  position: relative;
  margin-top: 600px;
}

#box {
  position: relative;
  left: 175px;
  width: 150px;
  height: 135px;
  background-color: lightblue;
  border: 2px solid;
}
<script src="https://polyfill.io/v2/polyfill.min.js?features=IntersectionObserver"></script>
<header>
  <button>NAVIGATION BUTTON TO HIDE</button>
</header>
  <div class="wrapper">
    <div id="box">
    </div>
  </div>
Noctule answered 15/3, 2019 at 11:32 Comment(6)
Good implementation, and according to the link in this answer it should work on safari by adding <!DOCTYPE html> to the HTMLJudenberg
Note that IntersectionObserver is an experimental feature (which may change in future).Somehow
@KarthikChintala - it is supported in every browser except IE - and there is also a polyfill available.Noctule
Doesn't address the OP's question since only detects changes: IntersectionObserver only fires callback after movement of the target relative to the root.Verbosity
When calling observe event is fired immediately telling you current intersection state of tracked element. So, in some way - it addresses.Postilion
This is currently the best up to date answer. It needs to be more visible among the top answers.Nell
H
26

I found it troubling that there wasn't a jQuery-centric version of the functionality available. When I came across Dan's solution I spied the opportunity to provide something for folks who like to program in the jQuery OO style. It's nice and snappy and works like a charm for me.

Bada bing bada boom

$.fn.inView = function(){
    if(!this.length) 
        return false;
    var rect = this.get(0).getBoundingClientRect();

    return (
        rect.top >= 0 &&
        rect.left >= 0 &&
        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
        rect.right <= (window.innerWidth || document.documentElement.clientWidth)
    );

};

// Additional examples for other use cases
// Is true false whether an array of elements are all in view
$.fn.allInView = function(){
    var all = [];
    this.forEach(function(){
        all.push( $(this).inView() );
    });
    return all.indexOf(false) === -1;
};

// Only the class elements in view
$('.some-class').filter(function(){
    return $(this).inView();
});

// Only the class elements not in view
$('.some-class').filter(function(){
    return !$(this).inView();
});

Usage

$(window).on('scroll',function(){

    if( $('footer').inView() ) {
        // Do cool stuff
    }
});
Hershel answered 2/8, 2015 at 13:40 Comment(3)
Would the curly brace be sufficient to close the if statement?Enaenable
I could not make it work with multiple elements of an identical class.Autoxidation
@TheWhizofOz i've updated my answer to give examples of the other possible use cases you've brought up. best of luck.Hershel
S
26

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;
}
Skeens answered 30/7, 2019 at 19:54 Comment(3)
How does this behave on mobile browsers? Most of them are buggy regarding viewport, with their header going up or down on scroll, and different behaviour when the keyboard shows up, depending if it's android or ios, etc.Satirist
@Satirist Should work just fine depending on when you call this method. If you call it and then resize the window, the result might obviously no longer be correct. You could call it on every resize-event depending on the type of functionality you want. Feel free to ask a separate question about your specific use case and ping me here.Skeens
In 99% of cases this is enough, specially if you just need to start or stop a fader or something and save some CPU. Its the developers that orientation devices to death, not common users. $(window).on('scroll', function(){ if(isInView($('.fader').get(0))) {} else {} });Telesthesia
A
17

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.

Animadversion answered 23/6, 2016 at 17:47 Comment(1)
it would be nice to have a function that returns not only the fact whether the given element is visible, but its square of the visible area like: visibleRectOfElement(el) => {top: 15, left: 45, right: 550, bottom: 420}Monolithic
U
10

I find that the accepted answer here is overly complicated for most use cases. This code does the job well (using jQuery) and differentiates between fully visible and partially visible elements:

var element         = $("#element");
var topOfElement    = element.offset().top;
var bottomOfElement = element.offset().top + element.outerHeight(true);
var $window         = $(window);

$window.bind('scroll', function() {

    var scrollTopPosition   = $window.scrollTop()+$window.height();
    var windowScrollTop     = $window.scrollTop()

    if (windowScrollTop > topOfElement && windowScrollTop < bottomOfElement) {
        // Element is partially visible (above viewable area)
        console.log("Element is partially visible (above viewable area)");

    } else if (windowScrollTop > bottomOfElement && windowScrollTop > topOfElement) {
        // Element is hidden (above viewable area)
        console.log("Element is hidden (above viewable area)");

    } else if (scrollTopPosition < topOfElement && scrollTopPosition < bottomOfElement) {
        // Element is hidden (below viewable area)
        console.log("Element is hidden (below viewable area)");

    } else if (scrollTopPosition < bottomOfElement && scrollTopPosition > topOfElement) {
        // Element is partially visible (below viewable area)
        console.log("Element is partially visible (below viewable area)");

    } else {
        // Element is completely visible
        console.log("Element is completely visible");
    }
});
Unnecessary answered 30/1, 2015 at 14:49 Comment(1)
You should definitely cache $window = $(window) outside the scroll handler.Jaleesa
I
8

ViewportInfo

/**
 * Returns Element placement information in Viewport
 * @link https://mcmap.net/q/33007/-how-can-i-tell-if-a-dom-element-is-visible-in-the-current-viewport
 *
 * @typedef {object} ViewportInfo - Whether the element is…
 * @property {boolean} isInViewport - fully or partially in the viewport
 * @property {boolean} isPartiallyInViewport - partially in the viewport
 * @property {boolean} isInsideViewport - fully inside viewport
 * @property {boolean} isAroundViewport - completely covers the viewport
 * @property {boolean} isOnEdge - intersects the edge of viewport
 * @property {boolean} isOnTopEdge - intersects the top edge
 * @property {boolean} isOnRightEdge - intersects the right edge
 * @property {boolean} isOnBottomEdge - is intersects the bottom edge
 * @property {boolean} isOnLeftEdge - is intersects the left edge
 *
 * @param el Element
 * @return {Object} ViewportInfo
 */
function getElementViewportInfo(el) {

    let result = {};

    let rect = el.getBoundingClientRect();
    let windowHeight = window.innerHeight || document.documentElement.clientHeight;
    let windowWidth  = window.innerWidth || document.documentElement.clientWidth;

    let insideX = rect.left >= 0 && rect.left + rect.width <= windowWidth;
    let insideY = rect.top >= 0 && rect.top + rect.height <= windowHeight;

    result.isInsideViewport = insideX && insideY;

    let aroundX = rect.left < 0 && rect.left + rect.width > windowWidth;
    let aroundY = rect.top < 0 && rect.top + rect.height > windowHeight;

    result.isAroundViewport = aroundX && aroundY;

    let onTop    = rect.top < 0 && rect.top + rect.height > 0;
    let onRight  = rect.left < windowWidth && rect.left + rect.width > windowWidth;
    let onLeft   = rect.left < 0 && rect.left + rect.width > 0;
    let onBottom = rect.top < windowHeight && rect.top + rect.height > windowHeight;

    let onY = insideY || aroundY || onTop || onBottom;
    let onX = insideX || aroundX || onLeft || onRight;

    result.isOnTopEdge    = onTop && onX;
    result.isOnRightEdge  = onRight && onY;
    result.isOnBottomEdge = onBottom && onX;
    result.isOnLeftEdge   = onLeft && onY;

    result.isOnEdge = result.isOnLeftEdge || result.isOnRightEdge ||
        result.isOnTopEdge || result.isOnBottomEdge;

    let isInX =
        insideX || aroundX || result.isOnLeftEdge || result.isOnRightEdge;
    let isInY =
        insideY || aroundY || result.isOnTopEdge || result.isOnBottomEdge;

    result.isInViewport = isInX && isInY;

    result.isPartiallyInViewport =
        result.isInViewport && result.isOnEdge;

    return result;
}
Irritate answered 24/12, 2021 at 21:4 Comment(0)
P
6

Here's my solution. It will work if an element is hidden inside a scrollable container.

Here's a demo (try re-sizing the window to)

var visibleY = function(el){
    var top = el.getBoundingClientRect().top, rect, el = el.parentNode;
    do {
        rect = el.getBoundingClientRect();
        if (top <= rect.bottom === false)
            return false;
        el = el.parentNode;
    } while (el != document.body);
    // Check it's within the document viewport
    return top <= document.documentElement.clientHeight;
};

I only needed to check if it's visible in the Y axis (for a scrolling Ajax load-more-records feature).

Philately answered 7/2, 2014 at 11:39 Comment(1)
This solution works only if element is hidden under the viewport. When scrolled on the upper side out of viewport, it still returns true.Slav
P
6

I think this is a more functional way to do it. Dan's answer do not work in a recursive context.

This function solves the problem when your element is inside others scrollable divs by testing any levels recursively up to the HTML tag, and stops at the first false.

/**
 * fullVisible=true only returns true if the all object rect is visible
 */
function isReallyVisible(el, fullVisible) {
    if ( el.tagName == "HTML" )
            return true;
    var parentRect=el.parentNode.getBoundingClientRect();
    var rect = arguments[2] || el.getBoundingClientRect();
    return (
            ( fullVisible ? rect.top    >= parentRect.top    : rect.bottom > parentRect.top ) &&
            ( fullVisible ? rect.left   >= parentRect.left   : rect.right  > parentRect.left ) &&
            ( fullVisible ? rect.bottom <= parentRect.bottom : rect.top    < parentRect.bottom ) &&
            ( fullVisible ? rect.right  <= parentRect.right  : rect.left   < parentRect.right ) &&
            isReallyVisible(el.parentNode, fullVisible, rect)
    );
};
Psilocybin answered 24/4, 2015 at 15:12 Comment(0)
B
5

The most accepted answers don't work when zooming in Google Chrome on Android. In combination with Dan's answer, to account for Chrome on Android, visualViewport must be used. The following example only takes the vertical check into account and uses jQuery for the window height:

var Rect = YOUR_ELEMENT.getBoundingClientRect();
var ElTop = Rect.top, ElBottom = Rect.bottom;
var WindowHeight = $(window).height();
if(window.visualViewport) {
    ElTop -= window.visualViewport.offsetTop;
    ElBottom -= window.visualViewport.offsetTop;
    WindowHeight = window.visualViewport.height;
}
var WithinScreen = (ElTop >= 0 && ElBottom <= WindowHeight);
Bulletin answered 17/11, 2019 at 9:37 Comment(0)
G
4

Based on dan's solution, I had a go at cleaning up the implementation so that using it multiple times on the same page is easier:

$(function() {

  $(window).on('load resize scroll', function() {
    addClassToElementInViewport($('.bug-icon'), 'animate-bug-icon');
    addClassToElementInViewport($('.another-thing'), 'animate-thing');
    // 👏 repeat as needed ...
  });

  function addClassToElementInViewport(element, newClass) {
    if (inViewport(element)) {
      element.addClass(newClass);
    }
  }

  function inViewport(element) {
    if (typeof jQuery === "function" && element instanceof jQuery) {
      element = element[0];
    }
    var elementBounds = element.getBoundingClientRect();
    return (
      elementBounds.top >= 0 &&
      elementBounds.left >= 0 &&
      elementBounds.bottom <= $(window).height() &&
      elementBounds.right <= $(window).width()
    );
  }

});

The way I'm using it is that when the element scrolls into view, I'm adding a class that triggers a CSS keyframe animation. It's pretty straightforward and works especially well when you've got like 10+ things to conditionally animate on a page.

Gilreath answered 26/12, 2014 at 21:0 Comment(1)
You should definitely cache $window = $(window) outside the scroll handlerUnnecessary
R
4

Most of the usages in previous answers are failing at these points:

-When any pixel of an element is visible, but not "a corner",

-When an element is bigger than viewport and centered,

-Most of them are checking only for a singular element inside a document or window.

Well, for all these problems I've a solution and the plus sides are:

-You can return visible when only a pixel from any sides shows up and is not a corner,

-You can still return visible while element bigger than viewport,

-You can choose your parent element or you can automatically let it choose,

-Works on dynamically added elements too.

If you check the snippets below you will see the difference in using overflow-scroll in element's container will not cause any trouble and see that unlike other answers here even if a pixel shows up from any side or when an element is bigger than viewport and we are seeing inner pixels of the element it still works.

Usage is simple:

// For checking element visibility from any sides
isVisible(element)

// For checking elements visibility in a parent you would like to check
var parent = document; // Assuming you check if 'element' inside 'document'
isVisible(element, parent)

// For checking elements visibility even if it's bigger than viewport
isVisible(element, null, true) // Without parent choice
isVisible(element, parent, true) // With parent choice

A demonstration without crossSearchAlgorithm which is usefull for elements bigger than viewport check element3 inner pixels to see:

function isVisible(element, parent, crossSearchAlgorithm) {
    var rect = element.getBoundingClientRect(),
            prect = (parent != undefined) ? parent.getBoundingClientRect() : element.parentNode.getBoundingClientRect(),
        csa = (crossSearchAlgorithm != undefined) ? crossSearchAlgorithm : false,
        efp = function (x, y) { return document.elementFromPoint(x, y) };
    // Return false if it's not in the viewport
    if (rect.right < prect.left || rect.bottom < prect.top || rect.left > prect.right || rect.top > prect.bottom) {
        return false;
    }
    var flag = false;
    // Return true if left to right any border pixel reached
    for (var x = rect.left; x < rect.right; x++) {
        if (element.contains(efp(rect.top, x)) || element.contains(efp(rect.bottom, x))) {
        flag = true;
        break;
      }
    }
    // Return true if top to bottom any border pixel reached
    if (flag == false) {
      for (var y = rect.top; y < rect.bottom; y++) {
        if (element.contains(efp(rect.left, y)) || element.contains(efp(rect.right, y))) {
          flag = true;
          break;
        }
      }
    }
    if(csa) {
      // Another algorithm to check if element is centered and bigger than viewport
      if (flag == false) {
        var x = rect.left;
        var y = rect.top;
        // From top left to bottom right
        while(x < rect.right || y < rect.bottom) {
          if (element.contains(efp(x,y))) {
            flag = true;
            break;
          }
          if(x < rect.right) { x++; }
          if(y < rect.bottom) { y++; }
        }
        if (flag == false) {
          x = rect.right;
          y = rect.top;
          // From top right to bottom left
          while(x > rect.left || y < rect.bottom) {
            if (element.contains(efp(x,y))) {
              flag = true;
              break;
            }
            if(x > rect.left) { x--; }
            if(y < rect.bottom) { y++; }
          }
        }
      }
    }
    return flag;
}

// Check multiple elements visibility
document.getElementById('container').addEventListener("scroll", function() {
    var elementList = document.getElementsByClassName("element");
  var console = document.getElementById('console');
    for (var i=0; i < elementList.length; i++) {
      // I did not define parent, so it will be element's parent
    if (isVisible(elementList[i])) {
          console.innerHTML = "Element with id[" + elementList[i].id + "] is visible!";
      break;
    } else {
        console.innerHTML = "Element with id[" + elementList[i].id + "] is hidden!";
    }
  }
});

// Dynamically added elements
for(var i=4; i <= 6; i++) {
  var newElement = document.createElement("div");
  newElement.id = "element" + i;
  newElement.classList.add("element");
  document.getElementById('container').appendChild(newElement);
}
#console { background-color: yellow; }
#container {
  width: 300px;
  height: 100px;
  background-color: lightblue;
  overflow-y: auto;
  padding-top: 150px;
  margin: 45px;
}
.element {
  margin: 400px;
  width: 400px;
  height: 320px;
  background-color: green;
}
#element3 {
  position: relative;
  margin: 40px;
  width: 720px;
  height: 520px;
  background-color: green;
}
#element3::before {
  content: "";
  position: absolute;
  top: -10px;
  left: -10px;
  margin: 0px;
  width: 740px;
  height: 540px;
  border: 5px dotted green;
  background: transparent;
}
<div id="console"></div>
<div id="container">
    <div id="element1" class="element"></div>
    <div id="element2" class="element"></div>
    <div id="element3" class="element"></div>
</div>

You see, when you are inside the element3 it fails to tell if it's visible or not, because we are only checking if the element is visible from sides or corners.

And this one includes crossSearchAlgorithm which allows you to still return visible when the element is bigger than the viewport:

function isVisible(element, parent, crossSearchAlgorithm) {
    var rect = element.getBoundingClientRect(),
            prect = (parent != undefined) ? parent.getBoundingClientRect() : element.parentNode.getBoundingClientRect(),
        csa = (crossSearchAlgorithm != undefined) ? crossSearchAlgorithm : false,
        efp = function (x, y) { return document.elementFromPoint(x, y) };
    // Return false if it's not in the viewport
    if (rect.right < prect.left || rect.bottom < prect.top || rect.left > prect.right || rect.top > prect.bottom) {
        return false;
    }
    var flag = false;
    // Return true if left to right any border pixel reached
    for (var x = rect.left; x < rect.right; x++) {
        if (element.contains(efp(rect.top, x)) || element.contains(efp(rect.bottom, x))) {
        flag = true;
        break;
      }
    }
    // Return true if top to bottom any border pixel reached
    if (flag == false) {
      for (var y = rect.top; y < rect.bottom; y++) {
        if (element.contains(efp(rect.left, y)) || element.contains(efp(rect.right, y))) {
          flag = true;
          break;
        }
      }
    }
    if(csa) {
      // Another algorithm to check if element is centered and bigger than viewport
      if (flag == false) {
        var x = rect.left;
        var y = rect.top;
        // From top left to bottom right
        while(x < rect.right || y < rect.bottom) {
          if (element.contains(efp(x,y))) {
            flag = true;
            break;
          }
          if(x < rect.right) { x++; }
          if(y < rect.bottom) { y++; }
        }
        if (flag == false) {
          x = rect.right;
          y = rect.top;
          // From top right to bottom left
          while(x > rect.left || y < rect.bottom) {
            if (element.contains(efp(x,y))) {
              flag = true;
              break;
            }
            if(x > rect.left) { x--; }
            if(y < rect.bottom) { y++; }
          }
        }
      }
    }
    return flag;
}

// Check multiple elements visibility
document.getElementById('container').addEventListener("scroll", function() {
    var elementList = document.getElementsByClassName("element");
  var console = document.getElementById('console');
    for (var i=0; i < elementList.length; i++) {
      // I did not define parent so it will be element's parent
    // and it will do crossSearchAlgorithm
    if (isVisible(elementList[i],null,true)) {
          console.innerHTML = "Element with id[" + elementList[i].id + "] is visible!";
      break;
    } else {
        console.innerHTML = "Element with id[" + elementList[i].id + "] is hidden!";
    }
  }
});
// Dynamically added elements
for(var i=4; i <= 6; i++) {
  var newElement = document.createElement("div");
  newElement.id = "element" + i;
  newElement.classList.add("element");
  document.getElementById('container').appendChild(newElement);
}
#console { background-color: yellow; }
#container {
  width: 300px;
  height: 100px;
  background-color: lightblue;
  overflow-y: auto;
  padding-top: 150px;
  margin: 45px;
}
.element {
  margin: 400px;
  width: 400px;
  height: 320px;
  background-color: green;
}
#element3 {
  position: relative;
  margin: 40px;
  width: 720px;
  height: 520px;
  background-color: green;
}
#element3::before {
  content: "";
  position: absolute;
  top: -10px;
  left: -10px;
  margin: 0px;
  width: 740px;
  height: 540px;
  border: 5px dotted green;
  background: transparent;
}
<div id="console"></div>
<div id="container">
    <div id="element1" class="element"></div>
    <div id="element2" class="element"></div>
    <div id="element3" class="element"></div>
</div>

JSFiddle to play with: http://jsfiddle.net/BerkerYuceer/grk5az2c/

This code is made for more precise information if any part of the element is shown in the view or not. For performance options or only vertical slides, do not use this! This code is more effective in drawing cases.

Rohrer answered 12/9, 2019 at 10:31 Comment(0)
K
2

A better solution:

function getViewportSize(w) {
    var w = w || window;
    if(w.innerWidth != null)
        return {w:w.innerWidth, h:w.innerHeight};
    var d = w.document;
    if (document.compatMode == "CSS1Compat") {
        return {
            w: d.documentElement.clientWidth,
            h: d.documentElement.clientHeight
        };
    }
    return { w: d.body.clientWidth, h: d.body.clientWidth };
}


function isViewportVisible(e) {
    var box = e.getBoundingClientRect();
    var height = box.height || (box.bottom - box.top);
    var width = box.width || (box.right - box.left);
    var viewport = getViewportSize();
    if(!height || !width)
        return false;
    if(box.top > viewport.h || box.bottom < 0)
        return false;
    if(box.right < 0 || box.left > viewport.w)
        return false;
    return true;
}
Kubetz answered 8/2, 2013 at 8:57 Comment(2)
You should try and explain why your version is better. As it stands, it looks more or less the same as the other solutions.Twotone
Great solution it has a BOX/ScrollContainer and is not using the WINDOW (only if its not specified). Take a look at the code than rate it is a more universal solution (Was searching for it a lot)Sialoid
S
2

I had the same question and figured it out by using getBoundingClientRect().

This code is completely 'generic' and only has to be written once for it to work (you don't have to write it out for each element that you want to know is in the viewport).

This code only checks to see if it is vertically in the viewport, not horizontally. In this case, the variable (array) 'elements' holds all the elements that you are checking to be vertically in the viewport, so grab any elements you want anywhere and store them there.

The 'for loop', loops through each element and checks to see if it is vertically in the viewport. This code executes every time the user scrolls! If the getBoudingClientRect().top is less than 3/4 the viewport (the element is one quarter in the viewport), it registers as 'in the viewport'.

Since the code is generic, you will want to know 'which' element is in the viewport. To find that out, you can determine it by custom attribute, node name, id, class name, and more.

Here is my code (tell me if it doesn't work; it has been tested in Internet Explorer 11, Firefox 40.0.3, Chrome Version 45.0.2454.85 m, Opera 31.0.1889.174, and Edge with Windows 10, [not Safari yet])...

// Scrolling handlers...
window.onscroll = function(){
  var elements = document.getElementById('whatever').getElementsByClassName('whatever');
  for(var i = 0; i != elements.length; i++)
  {
   if(elements[i].getBoundingClientRect().top <= window.innerHeight*0.75 &&
      elements[i].getBoundingClientRect().top > 0)
   {
      console.log(elements[i].nodeName + ' ' +
                  elements[i].className + ' ' +
                  elements[i].id +
                  ' is in the viewport; proceed with whatever code you want to do here.');
   }
};
Seraphine answered 13/9, 2015 at 13:59 Comment(0)
Z
2

Here is a function that tells if an element is in visible in the current viewport of a parent element:

function inParentViewport(el, pa) {
    if (typeof jQuery === "function"){
        if (el instanceof jQuery)
            el = el[0];
        if (pa instanceof jQuery)
            pa = pa[0];
    }

    var e = el.getBoundingClientRect();
    var p = pa.getBoundingClientRect();

    return (
        e.bottom >= p.top &&
        e.right >= p.left &&
        e.top <= p.bottom &&
        e.left <= p.right
    );
}
Zitazitah answered 28/9, 2018 at 10:1 Comment(0)
C
2

As simple as it can get, IMO:

function isVisible(elem) {
  var coords = elem.getBoundingClientRect();
  return Math.abs(coords.top) <= coords.height;
}
Custard answered 15/3, 2019 at 14:6 Comment(0)
A
2
 const isHTMLElementInView = (element: HTMLElement) => {
  const rect = element?.getBoundingClientRect()

  if (!rect) return
  return rect.top <= window.innerHeight && rect.bottom >= 0
 }

This function checks if the element is in the viewport on vertical level.

Alopecia answered 13/10, 2022 at 6:15 Comment(1)
In fact, in many cases, Y is the only axis you care aboutEnglebert
C
1

This checks if an element is at least partially in view (vertical dimension):

function inView(element) {
    var box = element.getBoundingClientRect();
    return inViewBox(box);
}

function inViewBox(box) {
    return ((box.bottom < 0) || (box.top > getWindowSize().h)) ? false : true;
}


function getWindowSize() {
    return { w: document.body.offsetWidth || document.documentElement.offsetWidth || window.innerWidth, h: document.body.offsetHeight || document.documentElement.offsetHeight || window.innerHeight}
}
Calabresi answered 2/1, 2015 at 12:17 Comment(0)
S
1

This is the easy and small solution that has worked for me.

Example: You want to see if the element is visible in the parent element that has overflow scroll.

$(window).on('scroll', function () {

     var container = $('#sidebar');
     var containerHeight = container.height();
     var scrollPosition = $('#row1').offset().top - container.offset().top;

     if (containerHeight < scrollPosition) {
         console.log('not visible');
     } else {
         console.log('visible');
     }
})
Snowman answered 19/6, 2017 at 10:49 Comment(0)
L
1

All the answers here are determining if the element is fully contained within the viewport, not just visible in some way. For example, if only half of an image is visible at the bottom of the view, the solutions here will fail, considering that "outside".

I had a use case where I'm doing lazy loading via IntersectionObserver, but due to animations that occur during pop-in, I didn't want to observe any images that were already intersected on page load. To do that, I used the following code:

const bounding = el.getBoundingClientRect();
const isVisible = (0 < bounding.top && bounding.top < (window.innerHeight || document.documentElement.clientHeight)) ||
        (0 < bounding.bottom && bounding.bottom < (window.innerHeight || document.documentElement.clientHeight));

This is basically checking to see if either the top or bottom bound is independently in the viewport. The opposite end may be outside, but as long as one end is in, it's "visible" at least partially.

Leith answered 12/3, 2020 at 3:19 Comment(0)
K
1

Here is a snippet to check if the given element is fully visible in its parent:

export const visibleInParentViewport = (el) => {
  const elementRect = el.getBoundingClientRect();
  const parentRect = el.parentNode.getBoundingClientRect();

  return (
    elementRect.top >= parentRect.top &&
    elementRect.right >= parentRect.left &&
    elementRect.top + elementRect.height <= parentRect.bottom &&
    elementRect.left + elementRect.width <= parentRect.right
  );
}
Kastroprauxel answered 16/4, 2021 at 11:24 Comment(0)
J
0

I use this function (it only checks if the y is inscreen since most of the time the x is not needed)

function elementInViewport(el) {
    var elinfo = {
        "top":el.offsetTop,
        "height":el.offsetHeight,
    };

    if (elinfo.top + elinfo.height < window.pageYOffset || elinfo.top > window.pageYOffset + window.innerHeight) {
        return false;
    } else {
        return true;
    }

}
Jemie answered 13/1, 2017 at 6:57 Comment(0)
T
-1

Domysee's answer https://stackoverflow.com/a/37998526 is close to correct.

Many examples use "completely contained in the viewport" and his code uses percentages to allow for partially visible. His code also addresses the "is a parent clipping the view" question, which most examples ignore.

One missing element is the impact of the parent's scrollbars - getBoundingClientRect returns the outer rectangle of the parent, which includes the scroll bars, not the inner rectangle, which doesn't. A child can hide behind the parent scroll bar and be considered visible when it isn't.

The recommended observer pattern isn't appropriate for my use case: using the arrow keys to change the currently selected row in a table, and make sure the new selection is visible. Using an observer for this would be excessively convoluted.

Here's some code -

it includes an additional hack (fudgeY) because my table has a sticky header that isn't detectable by straightforward means (and handling this automatically would be pretty tedious). Also, it uses decimal (0 to 1) instead of percentage for the required visible fraction. (For my case I need full y, and x isn't relevant).

function intersectRect(r1, r2) {
    var r = {};
    r.left = r1.left < r2.left ? r2.left : r1.left;
    r.top = r1.top < r2.top ? r2.top : r1.top;
    r.right = r1.right < r2.right ? r1.right : r2.right;
    r.bottom = r1.bottom < r2.bottom ? r1.bottom : r2.bottom;
    if (r.left < r.right && r.top < r.bottom)
        return r;
    return null;
}

function innerRect(e) {
    var b,r;
    b = e.getBoundingClientRect();
    r = {};
    r.left = b.left;
    r.top = b.top;
    r.right = b.left + e.clientWidth;
    r.bottom = b.top + e.clientHeight;
    return r;
}

function isViewable(e, fracX, fracY, fudgeY) {
    // ref https://stackoverflow.com/a/37998526
    // intersect all the rects and then check the result once
    // innerRect: mind the scroll bars
    // fudgeY: handle "sticky" thead in parent table.  Ugh.
    var r, pr, er;

    er = e.getBoundingClientRect();
    r = er;
    for (;;) {
        e = e.parentElement;
        if (!e)
            break;
        pr = innerRect(e);
        if (fudgeY)
            pr.top += fudgeY;
        r = intersectRect(r, pr);
        if (!r)
            return false;
    }

    if (fracX && ((r.right-r.left) / (er.right-er.left)) < (fracX-0.001))
        return false;
    if (fracY && ((r.bottom-r.top) / (er.bottom-er.top)) < (fracY-0.001))
        return false;
    return true;
}
Timpani answered 4/10, 2021 at 3:25 Comment(0)
M
-2

For a similar challenge, I really enjoyed this gist which exposes a polyfill for scrollIntoViewIfNeeded().

All the necessary Kung Fu needed to answer is within this block:

var parent = this.parentNode,
    parentComputedStyle = window.getComputedStyle(parent, null),
    parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')),
    parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')),
    overTop = this.offsetTop - parent.offsetTop < parent.scrollTop,
    overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight),
    overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft,
    overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth),
    alignWithTop = overTop && !overBottom;

this refers to the element that you want to know if it is, for example, overTop or overBottom - you just should get the drift...

Mckissick answered 14/3, 2016 at 13:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.