Bill Riess
Bill Riess

Reputation: 344

Check if element is partially in viewport

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!

Upvotes: 17

Views: 28979

Answers (6)

cloned
cloned

Reputation: 6742

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>

Upvotes: 2

folo
folo

Reputation: 524

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
  )
};

Upvotes: 40

Edwin
Edwin

Reputation: 769

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;

Upvotes: 0

Peter Kobzar
Peter Kobzar

Reputation: 39

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/

Upvotes: 2

Piper McCorkle
Piper McCorkle

Reputation: 1074

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.

Upvotes: 0

Yaron U.
Yaron U.

Reputation: 7881

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>

Upvotes: 4

Related Questions