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.

The offset of an element is its distance from a reference point.

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).

The reason for this generalisation is that it's the left and top edges of the web document where elements originate from. The transformational axis has its origin on the top-left point, and likewise it makes sense to model distances in these two points.
There's absolutely no issue in modeling offsets from the right and bottom of something; it's just that these reference points aren't used a lot and hence would feel unreal as we work with them!

Following is an illustration to clarify this:

Offset example

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.

This pair follows the normal graphical co-ordinate syntax in maths i.e (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).

The body element has 10px of margins around its ends.
Blue Box
width: 180px
height: 120px
Pink Box
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);
10 10
190 40

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.

One thing to remember while working with 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.

Had 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.

Live Example

If you look for a hidden gem in here, you'll see that:

Adding the scroll position and the top property, at any point, gives us the distance of an element from the top of the document.

You can confirm this very quickly:

  1. 0 + 1000px = 1000px (at the initial scroll position)
  2. 350px + 650px = 1000px (once we scroll for 350px)
  3. 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.

The reason we add 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?

For any element its nearest relative ancestor is defined as the first of its ancestor elements that has 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;
}
To understand the order of the four parameters to 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)
offsetLeft: 70
offsetTop: 130

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.

Remember to take into account border widths when calculating offsets on your own!

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 offsetParents 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!