16

I'm trying to determine if an element is partially or fully in the viewport.

I've found this which will determine if an element is fully in view but kept getting confused when trying to determine partial visibility. I don't want to use jQuery.

Basically, the idea is that there will be an element on the page that could be out of view. Once the user scrolls that element into view, even partially, it should trigger an event. I'll handle the event trigger by binding an onscroll event. I just need the detection to work properly.

function isInViewport(element) {
    var rect = element.getBoundingClientRect();
    var html = document.documentElement;
    return (
        rect.top >= 0 &&
        rect.left >= 0 &&
        rect.bottom <= (window.innerHeight || html.clientHeight) &&
        rect.right <= (window.innerWidth || html.clientWidth)
    );
}

Any help would be greatly appreciated!

Bill Riess
  • 334
  • 1
  • 3
  • 10
  • 1
    Instead of checking `top >= 0` and `bottom <= html.clientHeight`, you can flip them to get partial visibility, i.e. `bottom <= 0 && top >= html.clientHeight`. – Jackson Holiday Wheeler Nov 29 '19 at 07:39
  • 1
    Here is a great solution that allows you to choose full vs partial visibility: https://github.com/libshin/inViewport/blob/master/index.js – Jackson Holiday Wheeler Nov 29 '19 at 07:59
  • Use Intersection Observer API to Detect if Element is in Viewport, details here – https://frontendguruji.com/blog/how-to-detect-if-element-is-in-viewport-intersection-observer-api/ – Mandeep Pasbola Jan 31 '22 at 06:10

6 Answers6

36

Late answer, but about a month ago I wrote a function that does exactly that, it determines how much an element is visible measured in percent in the viewport. Ive tested it in chrome, firefox, ie11, ios on iphone/ipad. The function returns true when X percent (as a number from 0 to 100) of the element is visible. Only determines if the measurements of the element are visible and not if the element is hidden with opacity, visibility etc..

const isElementXPercentInViewport = function(el, percentVisible) {
  let
    rect = el.getBoundingClientRect(),
    windowHeight = (window.innerHeight || document.documentElement.clientHeight);

  return !(
    Math.floor(100 - (((rect.top >= 0 ? 0 : rect.top) / +-rect.height) * 100)) < percentVisible ||
    Math.floor(100 - ((rect.bottom - windowHeight) / rect.height) * 100) < percentVisible
  )
};
folo
  • 476
  • 4
  • 5
  • Cool function, this saved me so much time! – Michelangelo Jul 10 '19 at 15:10
  • 1
    This is pretty cool. Can someone explain what the `+-(rect.height / 1)` is supposed to do? If it's supposed to be flipping the sign of the height, then why not just `-rect.height`? – noblerare Mar 26 '21 at 20:49
  • you have a good point noblerera, it appears to be (quite clearly) redundant so i updated my answer in the post. – folo Mar 29 '21 at 08:55
  • Awesome job @folo – insivika Apr 06 '21 at 00:46
  • @folo this is perfect for capturing impressions of products when I have to scrape these for a client who does not have a dev team and I need to send this to GA/ GA4 – johnanish May 24 '21 at 09:36
  • so to just get how much of the element is visible, i just have to return `Math.floor(100 - ((rect.bottom - windowHeight) / rect.height) * 100)`. But what does the first part of the return statement in your solution signify? – Priyanker Rao Sep 13 '21 at 19:27
  • @PriyankerRao to be perfectly honest experience has tought me that you shouldnt write code like ive written it above unless a clear explanation follows to motivate the extremely compact way its written. the main reason for that is now i neither got the time or the memory to deconstruct it to properly to explain it to you so you understand what it does. let it be a lesson to anyone else who wants to make their code look "efficient". i mean it works, but on the top of my head i couldnt tell you why. – folo Sep 14 '21 at 09:01
  • Understood. But it's much easier and trustworthy to use this instead of any experimental APIs which may change in the future. – Priyanker Rao Sep 14 '21 at 12:42
  • percentVisible parameter work perfectly Thanks! – Akashxolotl Jul 20 '22 at 17:02
4

You need a solution based on element.offsetTop, element.offsetLeft, element.offsetHeight, element.offsetWidth, window.innerWidth and window.innerHeight

(depending on the situation, you might also want to take the scrolling position into consideration)

function isInViewport(element){
  if(element.offsetTop<window.innerHeight && 
       element.offsetTop>-element.offsetHeight
     && element.offsetLeft>-element.offsetWidth
     && element.offsetLeft<window.innerWidth){
      return true;
    } else {
      
      return false;
    }
}



function test(){
  alert(isInViewport(document.getElementById("elem"))?"Yes":"No"); 
}
#elem{width: 20px; height: 20px; background: red; }
#elem{position: absolute;top: -9px;left: 600px;}
    <div id="elem"></div>
    <button onclick="test()">Check</button>
Yaron U.
  • 7,681
  • 3
  • 31
  • 45
  • How can I factor scroll position into it? Basically, the idea is that there will be an element on the page that could be out of view. Once the user scrolls that element into view, even partially, it should trigger an event. I'll handle the event trigger by binding an onscroll event. I just need the detection to work properly. Thanks for your help thus far! – Bill Riess Jun 19 '15 at 17:46
  • 1
    http://stackoverflow.com/questions/2481350/retrieve-scrollbar-position-with-javascript – Henrique Barcelos Jun 19 '15 at 18:18
  • it says yes, and it's aposition is -9px, that means is not full visible – Karue Benson Karue Nov 08 '21 at 15:59
2

function partInViewport(elem) {
    let x = elem.getBoundingClientRect().left;
    let y = elem.getBoundingClientRect().top;
    let ww = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
    let hw = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
    let w = elem.clientWidth;
    let h = elem.clientHeight;
    return (
        (y < hw &&
         y + h > 0) &&
        (x < ww &&
         x + w > 0)
    );
}

document.addEventListener("scroll", ()=>{
 let el = document.getElementById("test");
 if (partInViewport(el)) {
   document.getElementById("container").style.backgroundColor = "green";
  } else {
   document.getElementById("container").style.backgroundColor = "red";
  }
});
#test {
  height: 200px;
  width: 145px;
  background-color: grey;
}
#container {
  height: 400px;
  width: 345px;
  transform: translate(400px, 360px);
  background-color: red;
  display: grid;
  align-items: center;
  justify-items: center;
}
body {
  height: 1500px;
  width: 1500px;
}
<div id="container">
  <div id="test"></div>
</div>

My example for this code: https://jsfiddle.net/xqpebwtv/27/

2

The modern way on how to handle this would be Intersection Observer (IO). With IO you can observe (as the name suggest) elements and trigger actions whenver an alement comes into view. You can set the percentages at which the observer is triggered (e.g. 10% in view, 90% in view, ... )

I really like this example from the linked page, there you have 4 different elements. Each with a different trigger percentage.

let observers = [];

startup = () => {
  let wrapper = document.querySelector(".wrapper");

  // Options for the observers

  let observerOptions = {
    root: null,
    rootMargin: "0px",
    threshold: []
  };

  // An array of threshold sets for each of the boxes. The
  // first box's thresholds are set programmatically
  // since there will be so many of them (for each percentage
  // point).

  let thresholdSets = [
    [],
    [0.5],
    [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
    [0, 0.25, 0.5, 0.75, 1.0]
  ];

  for (let i = 0; i <= 1.0; i += 0.01) {
    thresholdSets[0].push(i);
  }

  // Add each box, creating a new observer for each

  for (let i = 0; i < 4; i++) {
    let template = document.querySelector("#boxTemplate").content.cloneNode(true);
    let boxID = "box" + (i + 1);
    template.querySelector(".sampleBox").id = boxID;
    wrapper.appendChild(document.importNode(template, true));

    // Set up the observer for this box

    observerOptions.threshold = thresholdSets[i];
    observers[i] = new IntersectionObserver(intersectionCallback, observerOptions);
    observers[i].observe(document.querySelector("#" + boxID));
  }

  // Scroll to the starting position

  document.scrollingElement.scrollTop = wrapper.firstElementChild.getBoundingClientRect().top + window.scrollY;
  document.scrollingElement.scrollLeft = 750;
}

intersectionCallback = (entries) => {
  entries.forEach((entry) => {
    let box = entry.target;
    let visiblePct = (Math.floor(entry.intersectionRatio * 100)) + "%";

    box.querySelector(".topLeft").innerHTML = visiblePct;
    box.querySelector(".topRight").innerHTML = visiblePct;
    box.querySelector(".bottomLeft").innerHTML = visiblePct;
    box.querySelector(".bottomRight").innerHTML = visiblePct;
  });
}

startup();
body {
  padding: 0;
  margin: 0;
}

svg:not(:root) {
  display: block;
}

.playable-code {
  background-color: #f4f7f8;
  border: none;
  border-left: 6px solid #558abb;
  border-width: medium medium medium 6px;
  color: #4d4e53;
  height: 100px;
  width: 90%;
  padding: 10px 10px 0;
}

.playable-canvas {
  border: 1px solid #4d4e53;
  border-radius: 2px;
}

.playable-buttons {
  text-align: right;
  width: 90%;
  padding: 5px 10px 5px 26px;
}

.contents {
  position: absolute;
  width: 700px;
  height: 1725px;
}

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

.sampleBox {
  position: relative;
  left: 175px;
  width: 150px;
  background-color: rgb(245, 170, 140);
  border: 2px solid rgb(201, 126, 17);
  padding: 4px;
  margin-bottom: 6px;
}

#box1 {
  height: 300px;
}

#box2 {
  height: 175px;
}

#box3 {
  height: 350px;
}

#box4 {
  height: 100px;
}

.label {
  font: 14px "Open Sans", "Arial", sans-serif;
  position: absolute;
  margin: 0;
  background-color: rgba(255, 255, 255, 0.7);
  border: 1px solid rgba(0, 0, 0, 0.7);
  width: 3em;
  height: 18px;
  padding: 2px;
  text-align: center;
}

.topLeft {
  left: 2px;
  top: 2px;
}

.topRight {
  right: 2px;
  top: 2px;
}

.bottomLeft {
  bottom: 2px;
  left: 2px;
}

.bottomRight {
  bottom: 2px;
  right: 2px;
}
<template id="boxTemplate">
  <div class="sampleBox">
    <div class="label topLeft"></div>
    <div class="label topRight"></div>
    <div class="label bottomLeft"></div>
    <div class="label bottomRight"></div>
  </div>
</template>

<main>
  <div class="contents">
    <div class="wrapper">
    </div>
  </div>
</main>
cloned
  • 6,346
  • 4
  • 26
  • 38
0

What your code is saying is that:

  • The top side of the element must be below the top side of the window,
  • The left of the element must be to the right of the left side of the window,
  • The bottom side of the element must be to the top of the bottom side of the window, AND
  • The right side of the element must be to the left of the right side of the window

What you want:

  • The top side of the element must be below the top side of the window OR the bottom side of the element must be above the bottom side of the window, AND
  • The left side of the element must be to the right of the left side of the window OR the right side of the element must be to the left of the right side of the window

Take what you will from that, the code should be simple enough from here.

Piper McCorkle
  • 1,044
  • 13
  • 27
0

This should do it, offsets are not needed, since we are comparing client rectangles.

function isPartiallyVisibleInViewport(element, viewport) {
  var bound = element.getBoundingClientRect();
  var bound2 = viewport.getBoundingClientRect();
  return bound.bottom > bound2.top && bound.top < bound2.bottom;
}

This function only checks vertically and must be extended if you also want to check horizontally:

return bound.bottom > bound2.top && bound.top < bound2.bottom && bound.right > bound2.left && bound.left < bound2.right;
Edwin
  • 733
  • 8
  • 20