What are offsets?
While building applications and working with HTML DOM elements, it's quite common to come across the need for getting an element's distance from the top of the page, its parent or the viewport.
A basic use case of this could be to attach a scroll listener to monitor an element's appearance into the viewport. For this we would surely need to know its distance from the top of the web page to determine how much scrolling will bring it into view.
In technical terminology, we call this distance as the offset of the element.
This reference point could be the top or left of the web document; the viewport; or simply a given element.
Typically, we denote offsets in two axes - x-axis and y-axis - with distances from the left of something and with distances from the top of something respectively.
In other words, reference points for an element's offsets are usually the left and top edges of something (as discussed before, this could be the web document, the viewport, or another element).
Following is an illustration to clarify this:
See how the blue box is offset 500px from the top of the document and 300px from the left of the document.
If we were to say this out concisely, we could simply say that the blue box has the offsets (300px, 500px) relative to the document.
(x, y)
, except for that x
and y
this time are distances from the left and top of a given reference point.Determine the left and top offsets of all the boxes shown in the snippet below, relative to the document.
You may state the offsets of each box as a pair (x, y).
body
element has 10px of margins around its ends.width: 180px
height: 120px
width: 150px
height: 120px
margin-top: 30px
- Blue box:
(0, 0)
, Pink box:(150px, 30px)
- Blue box:
(10px, 10px)
, Pink box:(30px, 190px)
- Blue box:
(10px, 10px)
, Pink box:(190px, 40px)
Calculating offsets in JavaScript
Now that you know what offsets are, it's time to learn how to actually calculate them in JavaScript.
We'll start by the getBoundingClientRect()
method which we saw in the previous Bounding Box chapter.
getBoundingClientRect()
Calling getBoundingClientRect()
on an element returns an object containing useful information about its bounding box, such as its width, height - and even its offsets!
Two properties exist on this object which hold the left and top offsets of the element, relative to the viewport. They are left
and top
, respectively.
A positive value of top
indicates that the element is somewhere below the top of the viewport.
A negative value indicates that the element is somewhere above the top of the viewport and therefore out of view.
Same goes for left
.
What does a positive value for left
indicate, as returned by the getBoundingClientRect()
method on a given element.
- The element is to the right of the viewport's left edge.
- The element is to the left of the viewport's left edge.
Let's see a few examples...
Consider the code below:
<div id="blue-box">Blue box</div>
<div id="pink-box">Pink box</div>
body {
margin: 10px;
}
div {
height: 120px;
width: 180px;
float: left;
}
#blue-box {
background-color: #b2d0f8;
}
#pink-box {
background-color: #ffa9f8;
margin-top: 30px
}
Here we create the same two boxes as we did in the snippet above - this time, instead of doing the offset maths ourselves, we leave it to the JavaScript engine.
var blueBox = document.getElementById("blue-box"),
pinkBox = document.getElementById("pink-box");
// bounding rectangle for blueBox
var blueRect = blueBox.getBoundingClientRect();
// bounding rectangle for pinkBox
var pinkRect = pinkBox.getBoundingClientRect();
console.log(blueRect.left, blueRect.top);
console.log(pinkRect.left, pinkRect.top);
As you can confirm, the values logged to the console are synonymous with the offset values we calculated previously.
Now this was a simple use case of left
and top
- below we consider a slightly more complicated example to truly understand what's meant by getBoundingClientRect()
returning offsets relative to the viewport.
We've placed a div
element 1000px from the top of the HTML document on purpose.
<div>Some content</div>
body { margin: 0 }
div { margin-top: 1000px }
body { margin: 0 }
is given to remove the predefined margins on the <body>
element.First initially, when the webpage loads (and is at its 0px scroll position), we call getBoundingClientRect()
on div
and log its top
property. It returns the value 1000
, just as we expect it to.
getBoundingClientRect()
is that if we call it while our page is at its 0px scroll position, then what the method will return will, in effect, be the distance of the element from the left and top of the page as well.Afterwards, when we scroll for let's say 350px and then call getBoundingClientRect()
once again, its top
property returns 650
.
See how the value changes? The element is now only 650px far away from the top edge of the viewport.
top
been relative to the top of the document, instead of the viewport, its value would've always remained the same!If we continue scrolling and cross the 1000px mark to end, let's say, at 1520px of scroll, here is what will happen.
Calling getBoundingClientRect()
on div
will return an object whose top
property will be equal to -520
, since now the element is 520px above the viewport (1000px was its initial distance).
You can visualise all this in the link below.
If you look for a hidden gem in here, you'll see that:
top
property, at any point, gives us the distance of an element from the top of the document.You can confirm this very quickly:
- 0 + 1000px = 1000px (at the initial scroll position)
- 350px + 650px = 1000px (once we scroll for 350px)
- 1520px + (-520px) = 1000px
This means that if you want to get the offset of any element ele
relative to the top of the web document, you can reliably use the following statement:
ele.getBoundingClientRect().top + window.pageYOffset
Obviously you'll want to save this in a some variable, or identifer so that you can reuse it in conditional checks or whatever you need it for.
window.pageYOffset
is to ensure that if the browser automatically scrolls to some scroll position (for example, when we visit ID links) before our calculation is made, the calculated offset accounts for the performed scroll.offsetLeft
and offsetTop
Apart from getBoundingClientRect()
, we've got yet another way to compute offsets of an element; and this time not just relative to the viewport.
That is using the offsetLeft
and offsetTop
properties, available on Element
objects.
offsetLeft
returns the distance of an element from the left of its nearest relative ancestor whereas offsetTop
returns the distance of the element from the top of its nearest relative ancestor.
But what is the nearest relative ancestor?
position: relative
set or else the <body>
element.So for instance, if we have the following HTML then the nearest relative ancestor of p
would be the #ancestor2
element.
<div id="ancestor2" style="position: relative">
<div id="ancestor1">
<p>A paragraph</p>
</div>
</div>
This is simply because it has position: relative
set on it, as can be seen in line 1.
Similarly, in the HTML below, since none of the elements have position: relative
set, the nearest relative ancestor of p
will be taken the default document.body
object.
<body><!--This is the default relative ancestor-->
<div id="ancestor2">
<div id="ancestor1">
<p>A paragraph</p>
</div>
</div>
</body>
Now you ask: OK the term 'nearest relative ancestor' sounds interesting, but is there any way to get it in JavaScript? The answer is simply - yes!
For a given element, its offsetParent
property holds another element which is its nearest relative ancestor. If none exists, then the property simply holds a reference to document.body
.
Let's review the previous code above:
<div id="ancestor2" style="position: relative">
<div id="ancestor1">
<p>A paragraph</p>
</div>
</div>
Now with this in place, following we retrieve the nearest relative ancestor of p
and log its id
attribute:
var p = document.getElementsByTagName("p")[0];
console.log(p.offsetParent.id); // "ancestor2"
As expected, it turns out to be "ancestor2"
.
Similarly for the HTML below, p.offsetParent
will return the document.body
element.
<body><!--This is the default relative ancestor-->
<div id="ancestor2">
<div id="ancestor1">
<p>A paragraph</p>
</div>
</div>
</body>
var p = document.getElementsByTagName("p")[0];
console.log(p.offsetParent === document.body); // true
With offsetParent
out of the way, let's now experiment around with offsetLeft
and offsetTop
.
Consider the following code. We've got a p
element sitting inside a div
container, at a left margin of 70px and top margin of 130px. The div
container has position: relative
set and is therefore the nearest relative ancestor of p
.
<div>
<p>A paragraph</p>
</div>
body {margin: 10px}
div {
margin: 40px 0 0 30px;
border: 1px solid grey;
position: relative; /* this part is the most important here */
}
p {
margin: 130px 0 0 70px;
background-color: #e2b600;
}
margin
, please refer to CSS Margins.A paragraph
If we log the offsetLeft
and offsetTop
properties of the p
element, it should output its corresponding margin values:
var p = document.getElementsByTagName("p")[0];
console.log("offsetLeft:", p.offsetLeft)
console.log("offsetTop:", p.offsetTop)
The offsetParent
of p
is div
; likewise, its offsetTop
and offsetLeft
properties will be evaluated relative to this div
element.
However, removing just one style from div
, i.e position: relative
, could produce completely different results here.
In the code above, if div
doesn't has position: relative
set, then what will p
's offsetTop
and offsetLeft
properties return.
Without position: relative
on div
, the nearest relative ancestor of p
will be the body
element. Consequently, its offsetTop
and offsetLeft
properties will be computed relative to the body
element.
This means that offsetLeft
and offsetTop
will be equal to 111
(70 + 30 + 1 + 10) and 181
(130 + 40 + 1 + 10), respectively.
As you can clearly see, offsetLeft
and offsetTop
don't return an element's distance relative to the viewport, but rather to its offsetParent
.
Moving on, the offsetParent
of document.body
is the null
value.
This produces one really interesting consequence i.e the value of offsetParent
can be used to calculate the offset of any element relative to the top of the document, in an iterative/recursive manner.
Let's see whether you can figure out how to accomplish this idea! Your skills are at a test now!
Construct a function that takes in an element object and returns its distance from the top of the document using offsetTop
and offsetParent
.
If the element's offsetParent
is null
just return null
rightaway.
You may use the following setup:
function distanceTop(ele) {
// write your code here
}
The logic is superbly easy - walk your way up the chain of all offsetParent
s until it becomes null
, and sum up each one's offsetTop
as you do so.
function distanceTop(ele) {
if (ele.offsetParent === null) return null
var parent = ele;
var offset = 0;
while (parent !== null) {
offset += parent.offsetTop
parent = parent.offsetParent
}
return offset;
}
In conclusion
Knowing the purpose and significance of calculating offsets of elements relative to the document or the viewport on a webpage, is a crucial skill web developers must have.
Just go over the applications where offset computations are utilised and you'll understand this. We've got lazy loading, infinite scrolling, ad metrics, powering CSS features and way much more!
Likewise go through this chapter twice or thrice if the need be, as well as experiment around in the console until and unless you understand offsets to the core!. These are perhaps the best ways to learn anything the right way!