Search code examples
javascriptjquerycssscrolloffset

Why, using offset().top and scrollTop() within a scrolling element are my dimensions not returning as intended?


I'm developing a web app here http://101drums.com/test and wish to fade in and out elements as they enter and leave the visible part of the scrolling parent element. I have been helped by the answer by myfunkyside in this question Fade In on Scroll Down, Fade Out on Scroll Up - based on element position in window and have adapted this code here https://jsfiddle.net/tciprop/u4vaodvL/. However, either the elements offset top is returning too big and/or the visible part of the scrolling div is returning to low so that the fade creeps into the visible area. Any ideas? Here is my Fiddle https://jsfiddle.net/tciprop/u4vaodvL/.

HTML

<div class="header"></div>
<div class="menu">
  <div class="fade">Fade In 01</div>
  <div class="fade">Fade In 02</div>
  <div class="fade">Fade In 03</div>
  <div class="fade">Fade In 04</div>
  <div class="fade">Fade In 05</div>
  <div class="fade">Fade In 06</div>
  <div class="fade">Fade In 07</div>
  <div class="fade">Fade In 08</div>
  <div class="fade">Fade In 09</div>
  <div class="fade">Fade In 10</div>
  <div class="fade">Fade In 01</div>
  <div class="fade">Fade In 02</div>
  <div class="fade">Fade In 03</div>
  <div class="fade">Fade In 04</div>
  <div class="fade">Fade In 05</div>
  <div class="fade">Fade In 06</div>
  <div class="fade">Fade In 07</div>
  <div class="fade">Fade In 08</div>
  <div class="fade">Fade In 09</div>
  <div class="fade">Fade In 10</div>
</div>

CSS

body {
  overflow-x: hidden;
  overflow-y: hidden;
}
.header {
  height: 40px;
}
.fade {
  position: relative;
  margin: 10px 5px 10px 5px;
  padding: 10px 5px 10px 5px;
  background-color: lightgreen;
  opacity: 1;
  height: 50px;
}
.menu {
  position: relative;
  overflow-x: hidden;
  overflow-y: scroll;
  border: 2vw 3vh;
    width: 90vw;
  height: 80vh;
  margin: auto;
}

JS

$(window).on("load",function() {
  $(".menu").scroll(function() {
    $(".fade").each(function() {
        //find relative positions
        var objectTop = $(this).offset().top;
        var objectBottom = objectTop + $(this).outerHeight();
        var windowTop = $(".menu").scrollTop();
        var windowBottom = windowTop + $(".menu").innerHeight();

      if (objectTop < windowTop) {//fade out on leaving top of scrollable area
            if ($(this).css("opacity")==1) {
                    $(this).fadeTo(500,0);
            }
      } else if (objectBottom < windowBottom && objectTop > windowTop) {//fade in on entering top of scrollable area
            if ($(this).css("opacity")==0) {
                    $(this).fadeTo(500,1);
          }
      } else if (objectBottom < windowBottom) {//fade in on entering bottom of scrollable area
            if ($(this).css("opacity")==0) {
                    $(this).fadeTo(500,1);
          }
      } else {
            if ($(this).css("opacity")==1) {//fade out on leaving bottom of scrollable area
                    $(this).fadeTo(500,0);
            }
      }
     console.log(objectTop, objectBottom, windowTop, windowBottom);
    });
  }); $(".menu").scroll(); //invoke scroll-handler on page-load
});

Solution

  • Use .position().top instead of offset().top (I made some other changes too, see bullet points):

    $(window).on("load",function() {
        $(".menu").scroll(function() {
            var parentBottom = $(this).innerHeight();
            $(".fade").each(function() {
                var objectTop = $(this).position().top;
                var objectBottom = objectTop + $(this).outerHeight();
    
                if (0 <= objectTop&&objectBottom < parentBottom) { //fade in when object is completely within view
                    if ($(this).css("opacity")==0) {$(this).fadeTo(500,1);}
                } else { //fade out when object goes out of view (either top or bottom)
                    if ($(this).css("opacity")==1) {$(this).fadeTo(500,0);}
                }
            });
        }).scroll(); //invoke scroll-handler on page-load
    });
    

    $(window).on("load",function() {
      $(".menu").scroll(function() {
        var parentBottom = $(this).innerHeight();
        $(".fade").each(function() {
          var objectTop = $(this).position().top;
          var objectBottom = objectTop + $(this).outerHeight();
          
          if (0 <= objectTop&&objectBottom < parentBottom) { //fade in when object is completely within view
            if ($(this).css("opacity")==0) {$(this).fadeTo(500,1);}
          } else { //fade out when object goes out of view (either top or bottom)
            if ($(this).css("opacity")==1) {$(this).fadeTo(500,0);}
          }
        });
      }).scroll(); //invoke scroll-handler on page-load
    });
    body {
      overflow-x: hidden;
      overflow-y: hidden;
    }
    .header {
      height: 40px;
      border: 2px solid red;
    }
    .menu {
      position: relative;
      width: 90vw;
      height: 80vh;
      margin: auto;
      border: 1px solid blue;
      border-width: 2vw 3vh;
      overflow-x: hidden;
      overflow-y: scroll;
    }
    .fade {
      position: relative;
      height: 50px;
      margin: 10px 5px;
      padding: 10px 5px;
      background-color: lightgreen;
      opacity: 1;
    }
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    
    <div class="header"></div>
    <div class="menu">
      <div class="fade">Fade In 01</div>
      <div class="fade">Fade In 02</div>
      <div class="fade">Fade In 03</div>
      <div class="fade">Fade In 04</div>
      <div class="fade">Fade In 05</div>
      <div class="fade">Fade In 06</div>
      <div class="fade">Fade In 07</div>
      <div class="fade">Fade In 08</div>
      <div class="fade">Fade In 09</div>
      <div class="fade">Fade In 10</div>
      <div class="fade">Fade In 11</div>
      <div class="fade">Fade In 12</div>
      <div class="fade">Fade In 13</div>
      <div class="fade">Fade In 14</div>
      <div class="fade">Fade In 15</div>
      <div class="fade">Fade In 16</div>
      <div class="fade">Fade In 17</div>
      <div class="fade">Fade In 18</div>
      <div class="fade">Fade In 19</div>
      <div class="fade">Fade In 20</div>
    </div>
    jsfiddle: https://jsfiddle.net/2bc5ueLc/3/

    • .scrollTop isn't necessary anymore, I changed it to 0.
    • I moved parentBottom (windowBottom) out of the .each, because that value doesn't change and so only needs to be stored once.
    • I simplified your if-clause a bit.

    UPDATE

    If you want to fade the elements based on their visible percentage, use the following code:

    $(window).on("load",function() {
        $(".menu").scroll(function() {
            var parentBottom = $(this).innerHeight();
            $(".fade").each(function() {
                var objectHeight = $(this).outerHeight();
                var objectTop = $(this).position().top;
                var objectBottom = objectTop + objectHeight;
    
                //fade in/out based on visible percentage of element
                if (objectTop < 0) {
                    if (objectBottom > 0) {$(this).fadeTo(0,objectBottom/objectHeight);}
                    else if ($(this).css("opacity")!=0) {$(this).fadeTo(0,0);}
                } else if (objectBottom > parentBottom) {
                    if (objectTop < parentBottom) {$(this).fadeTo(0,(parentBottom-objectTop)/objectHeight);}
                    else if ($(this).css("opacity")!=0) {$(this).fadeTo(0,0);}
                } else if ($(this).css("opacity")!=1) {$(this).fadeTo(0,1);}
            });
        }).scroll(); //invoke scroll-handler on page-load
    });
    

    $(window).on("load",function() {
      $(".menu").scroll(function() {
        var parentBottom = $(this).innerHeight();
        $(".fade").each(function() {
          var objectHeight = $(this).outerHeight();
          var objectTop = $(this).position().top;
          var objectBottom = objectTop + objectHeight;
          
          //fade in/out based on visible percentage of element
          if (objectTop < 0) {
            if (objectBottom > 0) {$(this).fadeTo(0,objectBottom/objectHeight);}
            else if ($(this).css("opacity")!=0) {$(this).fadeTo(0,0);}
          } else if (objectBottom > parentBottom) {
            if (objectTop < parentBottom) {$(this).fadeTo(0,(parentBottom-objectTop)/objectHeight);}
            else if ($(this).css("opacity")!=0) {$(this).fadeTo(0,0);}
          } else if ($(this).css("opacity")!=1) {$(this).fadeTo(0,1);}
        });
      }).scroll(); //invoke scroll-handler on page-load
    });
    body {
      overflow-x: hidden;
      overflow-y: hidden;
    }
    .header {
      height: 40px;
      border: 2px solid red;
    }
    .menu {
      position: relative;
      width: 90vw;
      height: 80vh;
      margin: auto;
      border: 1px solid blue;
      border-width: 2vw 3vh;
      overflow-x: hidden;
      overflow-y: scroll;
    }
    .fade {
      position: relative;
      height: 50px;
      margin: 10px 5px;
      padding: 10px 5px;
      background-color: lightgreen;
      opacity: 1;
    }
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    
    <div class="header"></div>
    <div class="menu">
      <div class="fade">Fade In 01</div>
      <div class="fade">Fade In 02</div>
      <div class="fade">Fade In 03</div>
      <div class="fade">Fade In 04</div>
      <div class="fade">Fade In 05</div>
      <div class="fade">Fade In 06</div>
      <div class="fade">Fade In 07</div>
      <div class="fade">Fade In 08</div>
      <div class="fade">Fade In 09</div>
      <div class="fade">Fade In 10</div>
      <div class="fade">Fade In 11</div>
      <div class="fade">Fade In 12</div>
      <div class="fade">Fade In 13</div>
      <div class="fade">Fade In 14</div>
      <div class="fade">Fade In 15</div>
      <div class="fade">Fade In 16</div>
      <div class="fade">Fade In 17</div>
      <div class="fade">Fade In 18</div>
      <div class="fade">Fade In 19</div>
      <div class="fade">Fade In 20</div>
    </div>
    jsfiddle: https://jsfiddle.net/2bc5ueLc/6/