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.)
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.)
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
);
}
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>
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>
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;
}
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);
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
visiblewhen only a pixel from any sides shows up and is not a corner,-You can still return
visiblewhile element bigger than viewport,-You can choose your
parent elementor 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.
As simple as it can get, IMO:
function isVisible(elem) {
var coords = elem.getBoundingClientRect();
return Math.abs(coords.top) <= coords.height;
}
/**
* Returns Element placement information in Viewport
* @link https://stackoverflow.com/a/70476497/2453148
*
* @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;
}
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
);
}
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.
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
);
}
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;
}