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:
#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.
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.