Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

jQuery offset() broken by body position:relative combined with element margin

This isn't a bug because the behaviour is consistent across FF, Chrome, IE9 and Safari on Win7.

The app i'm working on is 3rd party to the host page, so the CSS is immutable.The script attempts to align a new div with an existing element.

  • The body is position:relative
  • There is an H1 at the top of the page
  • The margin from the H1 seems to change where the body 0,0 is calculated from - even though the background on the body extends all the way to the edge, and it's offsetTop property reports 0
  • Setting a border on the body solves the problem - seems strange behaviour but is consistent across browsers? (not a viable solution)
  • Removing the H1 margins solves the problem (not a viable solution)

Example here, the JS is commented for replicating each case:
http://codepen.io/anon/pen/EGvlb

I don't think this is a bug with jQuery - it seems to be down to a legitimate relationship between the H1 margin and the body element?

$(function(){

  /* Setting body to position:relative breaks offset()
     because H1 margin moves body down */
  $(document.body).css({position: "relative"});

  /* Strange: putting a border on body fixes things? */
  //$(document.body).css({border: "1px solid #000"});

  /* Removing H1 margin removes problem */
  //$("h1").css({margin: 0});

  $("#overlay").css({
      left: $("#existing").offset().left,
      top: $("#existing").offset().top
  })
});
like image 931
robC Avatar asked Nov 14 '12 18:11

robC


1 Answers

I don't think this is a bug with jQuery - it seems to be down to a legitimate relationship between the H1 margin and the body element?

Yeah: the margins of the body and the h1 are collapsing, so not only does the h1 get shifted down by its default margin, but so does body. This is why you were able to make these observations:

  • Setting a border on the body solves the problem - seems strange behaviour but is consistent across browsers? (not a viable solution)
  • Removing the H1 margins solves the problem (not a viable solution)

Anyway, by setting the body to position: relative, you're telling #overlay to absolutely position itself with body's top offset as the origin. Your script then moves it relative to the offset of #existing. The value of its offsetTop is relative to the viewport, which is calculated based on the position of this box... which itself is relative to h1 because it directly follows the h1 in normal flow.

Because the margins of the h1 and body elements are collapsing, they have the same rendered margins. What happens then is twofold:

  1. #existing is pushed down by the h1 as its following sibling in normal flow. This increases its offset from the viewport (in your CodePen test case, it's the top of the preview pane), resulting in its offsetTop being greater.

  2. When the body is set to position: relative, #overlay ends up getting positioned with its origin set to that of the body, which itself has also been pushed down due to collapsing with the h1's margin. This increases the top offset of its origin, which would not happen if you had not positioned the body, because then #overlay uses the origin of the viewport instead, which never moves.

The distance of #existing from the top of the viewport is added on to the offset of #overlay from its origin, that being the body, resulting in this effect.

In summary: due to margin collapse, the browser ends up having to account for downward shifts in both h1 and body. The h1 influences #existing and in turn its offsetTop, while the body influences #overlay when you position it relatively. So the shifting effect appears twofold.

As a quick workaround, you can subtract the h1 margins from the offsets of #existing for the purposes of offsetting #overlay:

  $("#overlay").css({
      left: $("#existing").offset().left - $("h1").offset().left,
      top: $("#existing").offset().top - $("h1").offset().top
  })

Updated example

Notice that using $(document.body) instead of $("h1") above won't work. This is because margin collapse is purely a rendering effect, and does not actually influence the body's offsets until you give it its own margin (in other words, the body margins are still computing to zero, so the DOM remains none the wiser).


Not directly related to the issue at hand, but worth addressing anyway:

  • The margin from the H1 seems to change where the body 0,0 is calculated from - even though the background on the body extends all the way to the edge, and it's offsetTop property reports 0

The first and last parts of this point have been addressed above.

As for the body background: while it appears to extend all the way to the edge, this extended background does not actually belong to body - it is propagated from body to, and thus belongs to, the root element (html) and the viewport. See this answer for an explanation.

Oh and this:

This isn't a bug because the behaviour is consistent across FF, Chrome, IE9 and Safari on Win7.

Made me smile. Because for once, somebody is right in saying it.

like image 81
BoltClock Avatar answered Nov 12 '22 16:11

BoltClock