17

There's a div (brown rectangle) on the page. The page is higher than the viewport (orange rectangle) so it can be scrolled, which means that the div might only partially show up or not at all.

element visibility

What's the simplest algorithm to tell how much % of the div is visible in the viewport?

(To make things easier, the div always fits into the viewport horizontally, so only the Y axis needs to be considered at the calculations.)

tom
  • 2,137
  • 2
  • 27
  • 51
  • Many else ifs with scrollTop and /scrollBottom (scrollTop+element.height): Two different cases for 0%s, one case when it's fully visible, and two other different cases if it's partially visible top and bottom. Many ifs, and then many calculations inside these blocks, and it feels super redundant. So it seemed too complicated, then I got the idea that the whole thing is axially symmetric from the middle of the viewport, the calculations deliver the same values just with different signs, so I thought there should be a simpler, more universal way doing it (abs?) than many ifs, that's why I asked. – tom Nov 22 '15 at 21:05
  • 1
    3 `if`s, not much more : ) just store as much values as you can instead of making jQuery calls all the time. Both answers quite illustrate this. – sodawillow Nov 22 '15 at 22:31

5 Answers5

27

See one more example in fiddle: https://jsfiddle.net/1hfxom6h/3/

/*jslint browser: true*/
/*global jQuery, window, document*/
(function ($) {
    'use strict';
    var results = {};

    function display() {
        var resultString = '';

        $.each(results, function (key) {
            resultString += '(' + key + ': ' + Math.round(results[key]) + '%)';
        });

        $('p').text(resultString);
    }

    function calculateVisibilityForDiv(div$) {
        var windowHeight = $(window).height(),
            docScroll = $(document).scrollTop(),
            divPosition = div$.offset().top,
            divHeight = div$.height(),
            hiddenBefore = docScroll - divPosition,
            hiddenAfter = (divPosition + divHeight) - (docScroll + windowHeight);

        if ((docScroll > divPosition + divHeight) || (divPosition > docScroll + windowHeight)) {
            return 0;
        } else {
            var result = 100;

            if (hiddenBefore > 0) {
                result -= (hiddenBefore * 100) / divHeight;
            }

            if (hiddenAfter > 0) {
                result -= (hiddenAfter * 100) / divHeight;
            }

            return result;
        }
    }

    function calculateAndDisplayForAllDivs() {
        $('div').each(function () {
            var div$ = $(this);
            results[div$.attr('id')] = calculateVisibilityForDiv(div$);
        });

        display();
    }

    $(document).scroll(function () {
        calculateAndDisplayForAllDivs();
    });

    $(document).ready(function () {
        calculateAndDisplayForAllDivs();
    });
}(jQuery));
div {
    height:200px;
    width:300px;

    border-width:1px;
    border-style:solid;
}
p {
    position: fixed;
    left:320px;
    top:4px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="div1">div1</div>
<div id="div2">div2</div>
<div id="div3">div3</div>
<div id="div4">div4</div>
<p id="result"></p>
Stanislav
  • 576
  • 3
  • 11
5

Here's a snippet illustrating how you can calculate this.

I've put the % values in the boxes for readability, and it even kinda "follows" the viewport ^^ :

Fiddle version

function listVisibleBoxes() {

  var results = [];

  $("section").each(function () {

    var screenTop = document.documentElement.scrollTop;
    var screenBottom = document.documentElement.scrollTop + $(window).height();
    var boxTop = $(this).offset().top;
    var boxHeight = $(this).height();
    var boxBottom = boxTop + boxHeight;

    if(boxTop > screenTop) {
      if(boxBottom < screenBottom) {
        //full box
        results.push(this.id + "-100%");
        $(this).html("100%").css({ "line-height": "50vh" });
      } else if(boxTop < screenBottom) {
        //partial (bottom)
        var percent = Math.round((screenBottom - boxTop) / boxHeight * 100) + "%";
        var lineHeight = Math.round((screenBottom - boxTop) / boxHeight * 50) + "vh";
        results.push(this.id + "-" + percent);
        $(this).html(percent).css({ "line-height": lineHeight });
      }
    } else if(boxBottom > screenTop) {
      //partial (top)
      var percent = Math.round((boxBottom - screenTop) / boxHeight * 100) + "%";
      var lineHeight = 100 - Math.round((boxBottom - screenTop) / boxHeight * 50) + "vh";
      results.push(this.id + "-" + percent);
      $(this).html(percent).css({ "line-height": lineHeight });
    }
  });

  $("#data").html(results.join(" | "));

}

$(function () {

  listVisibleBoxes();

  $(window).on("scroll", function() {
    listVisibleBoxes();
  });

});
body {
  background-color: rgba(255, 191, 127, 1);
  font-family: Arial, sans-serif;
}

section {
  background-color: rgba(175, 153, 131, 1);
  height: 50vh;
  font-size: 5vh;
  line-height: 50vh;
  margin: 10vh auto;
  overflow: hidden;
  text-align: center;
  width: 50vw;
}

#data {
  background-color: rgba(255, 255, 255, .5);
  left: 0;
  padding: .5em;
  position: fixed;
  top: 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<section id="one"></section>
<section id="two"></section>
<section id="three"></section>
<section id="four"></section>
<section id="five"></section>
<section id="six"></section>

<div id="data">data here</div>
sodawillow
  • 12,497
  • 4
  • 34
  • 44
4

After playing around a bit I think I've found perhaps the simplest way to do it: I basically determine how much the element extends over the viewport (doesn't matter in which direction) and based on this it can easily be calculated how much of it is visible.

// When the page is completely loaded.
$(document).ready(function() {

  // Returns in percentages how much can be seen vertically
  // of an element in the current viewport.
  $.fn.pvisible = function() {
    var eTop = this.offset().top;
    var eBottom = eTop + this.height();
    var wTop = $(window).scrollTop();
    var wBottom = wTop + $(window).height();
    var totalH = Math.max(eBottom, wBottom) - Math.min(eTop, wTop);
    var wComp = totalH - $(window).height();
    var eIn = this.height() - wComp;
    return (eIn <= 0 ? 0 : eIn / this.height() * 100);
  }

  // If the page is scrolled.
  $(window).scroll(function() {
    // Setting the opacity of the divs.
    $("div").each(function() {
      $(this).css("opacity", Math.round($(this).pvisible()) / 100);
    });
  });

});
html,
body {
  width: 100%;
  height: 100%;
}
body {
  background-color: rgba(255, 191, 127, 1);
}
div {
  width: 60%;
  height: 30%;
  margin: 5% auto;
  background-color: rgba(175, 153, 131, 1);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>

A little illustration to help understand how it works:

enter image description here

tom
  • 2,137
  • 2
  • 27
  • 51
  • The problem I have with this is that when the element is higher than the window height, it says for example it's 30% in viewport. That is correct in some way. But the logic I would like to see is that it calls that 100% in viewport. So even if it's a part of a element but it covers the full height of the screen, it should be 100% in viewport. How would I implement that in this script? – Floris Oct 18 '18 at 09:46
4

Chrome now supports Intersection Observer API

Example (TypeScript):

export const elementVisibleInPercent = (element: HTMLElement) => {
    return new Promise((resolve, reject) => {
        const observer = new IntersectionObserver((entries: IntersectionObserverEntry[]) => {
            entries.forEach((entry: IntersectionObserverEntry) => {
                resolve(Math.floor(entry.intersectionRatio * 100));
                clearTimeout(timeout);
                observer.disconnect();
            });
        });

        observer.observe(element);
        // Probably not needed, but in case something goes wrong.
        const timeout = setTimeout(() => {
            reject();
        }, 500);
    });
};

const example = document.getElementById('example');
const percentageVisible = elementVisibleInPercent(example);

Example (JavaScript):

export const elementVisibleInPercent = (element) => {
    return new Promise((resolve, reject) => {
        const observer = new IntersectionObserver(entries => {
            entries.forEach(entry => {
                resolve(Math.floor(entry.intersectionRatio * 100));
                clearTimeout(timeout);
                observer.disconnect();
            });
        });

        observer.observe(element);
        // Probably not needed, but in case something goes wrong.
        const timeout = setTimeout(() => {
            reject();
        }, 500);
    });
};

const example = document.getElementById('example');
const percentageVisible = elementVisibleInPercent(example);
Jompis
  • 640
  • 1
  • 8
  • 18
0

Please note that the Intersection Observer API is available since then, made specifically for this purpose.

tom
  • 2,137
  • 2
  • 27
  • 51