1

In first demo I'm using jquery 2.1.1 and getting offset top of a tag 22 in chrome, and its remains same while scrolling,

but when I'm using jquery 3.1.1 offset changes while scrolling.

Demo 1 with jquery 2.1.1

console.log($('a').offset().top);
$(window).scroll(function() {
  console.log($('a').offset().top);
})
body {
  height: 1200px;
}
a {
  font-size: 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#">hidden anchor</a>

Demo 2 with jquery 3.1.1

console.log($('a').offset().top);
$(window).scroll(function() {
  console.log($('a').offset().top);
})
body {
  height: 1200px;
}
a {
  font-size: 0;
  display: inline-block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<a href="#">hidden anchor</a>

Why this is happening.

Abhishek Pandey
  • 13,302
  • 8
  • 38
  • 68
  • 3
    there was some breaking changes to `offset`: https://jquery.com/upgrade-guide/3.0/#offset – Daniel A. White Jan 24 '17 at 13:36
  • 1
    Broken in 2.1.1; working in 3.1.1 - When you scroll down, it makes sense for the offset of a single element to change. Think of the top of the window as 0. Anything below this line is `+`, and anything above is `-`. As the element goes up (you scroll down), the offset decreases. – J. Titus Jan 24 '17 at 13:39
  • Possible duplicate of [jQuery offset().top returns wrong value - error with margin](http://stackoverflow.com/questions/29277608/jquery-offset-top-returns-wrong-value-error-with-margin) – Giacomo Paita Jan 24 '17 at 13:40
  • @J.Titus, I don't think that's correct: `offset()` is supposed to return the position relative to the document; what you're describing would be relative to the viewport. – Daniel Beck Jan 24 '17 at 13:49

2 Answers2

2

jQuery 2 has the correct behavior; offset() is relative to the document, so scroll position should not change it.

Note the emphasized bit here:

When using the .offset() method, the first item in the jQuery collection must be a DOM element that has a DOM getBoundingClientRect() method. (All browsers supported by jQuery 3.0 have this API.) Any other input may result in jQuery throwing an error. Also note that the element must be visible and currently in a document (i.e., not disconnected). https://jquery.com/upgrade-guide/3.0/#offset

When the element is visible, you get correct behavior in jQuery 3 (the offset() remains the same regardless of scroll position) but when the element is hidden using font-size:0, then offset() incorrectly returns a viewport-relative value (as far as I can tell, it's passing through the viewport-relative getBoundingClientRect() value for hidden elements. I don't know why it would do that, but the numbers match up).

console.log($('a.visible').offset().top);
console.log($('a.visible')[0].getBoundingClientRect();
console.log($('a.hidden').offset().top);
console.log($('a.hidden')[0].getBoundingClientRect();
$(window).scroll(function() {
   console.log($('a.visible').offset().top);
   console.log($('a.visible')[0].getBoundingClientRect();
   console.log($('a.hidden').offset().top);
   console.log($('a.hidden')[0].getBoundingClientRect();
})
body {
  height: 1200px;
}
a {
  display: inline-block;
}

a.hidden {
    font-size: 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<a class="visible" href="#">Not-hidden anchor</a>
<a class="hidden" href="#">hidden anchor</a>

The best workaround would seem to be to not try to test the position of a hidden element; use some container element with a real document position instead.

Daniel Beck
  • 20,653
  • 5
  • 38
  • 53
1

As many have pointed out, it's due to changes in the way offset works with hidden elements. I've dug through the code, and came across the piece of code that is (was) responsible for that behaviour:

rect = elem.getBoundingClientRect();

    // Make sure element is not hidden (display: none)
    if ( rect.width || rect.height ) { //this line is responsible
        doc = elem.ownerDocument;
        win = getWindow( doc );
        docElem = doc.documentElement;

        return {
            top: rect.top + win.pageYOffset - docElem.clientTop,
            left: rect.left + win.pageXOffset - docElem.clientLeft
        };
    }

    // Return zeros for disconnected and hidden elements (gh-2310)
    return rect;

The relevant line is if (rect.width || rect.height) which means, if the element has no height or width, it skips that part and returns the relative position.

However, it seems this has been fixed in the current code, so it should work with newer releases again. Now jQuery doesn't check for the width or height of getBoundingClientRect(), but for results of getClientRects().

if ( !elem.getClientRects().length ) {
  return { top: 0, left: 0 };
}

When having a text hidden with font-size:0, getClientRects() returns an array with some values, whereas the result of a really hidden element (display:none) returns an empty array, so the newer condition will work for "soft"(?)-hidden elements again.

console.log(document.getElementById('hidden').getClientRects());

console.log(document.getElementById('removed').getClientRects());
#hidden{
  font-size:0;
 }
#removed{
   display:none;  
}
<div id="hidden">I'm hidden.</div>
<div id="removed">I'm display:none</div>
Thomas Altmann
  • 1,744
  • 2
  • 11
  • 16