Course: JavaScript

Progress (0%)

CSSOM Elements

Chapter 57 55 mins

Learning outcomes:

  1. The offsetLeft/offsetTop and offsetParent properties
  2. Getting the dimensions of the border box via offsetWidth/offsetHeight
  3. The clientLeft/clientTop properties
  4. Getting the dimensions of the padding box via clientWidth/clientHeight
  5. The scrollLeft/scrollTop properties
  6. Getting the dimensions of the content via scrollWidth/scrollHeight
  7. The scrollIntoView() method

Introduction

In the last chapter, we saw a good bunch of properties and methods available on window as global identifiers or an element nodes that relate, in one way or the other, to the client, i.e. the browser window.

Now in this chapter, it's time to focus solely on element nodes and see a couple of properties and methods available on them as per the CSSOM specification. We'll start off with the five properties offsetLeft, offsetTop, offsetParent, offsetWidth and offsetHeight, where the former three allow us to retrieve the position of an element form the document.

Then, we'll move over to consider the properties clientLeft, clientTop, clientWidth and clientHeight. Note however that unlike the usage of the word 'client', these properties don't apply to the browser window, but rather to the padding box of a given element.

Finally, we'll consider a couple of scrollling utilities as well such as scrollLeft, scrollTop, scrollWidth and scrollHeight. And we'll also consider a handful of methods including scrollIntoView().

Let's begin.

Getting the offsets of an element

The two properties offsetLeft and offsetTop allow us to retrieve the offset of an element from its nearest positioned ancestor.

But what exactly is an offset and the nearest positioned ancestor?

Starting with the former,

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

On web pages, an element's offset could refer to many kinds of distances, such as the distance of the element from the top edge of the document, the distance from the top edge of the viewport, or the distance from another element.

The context in which we use the word 'offset' distinguishes between each kind. Furthermore, typically, as per the co-ordinate system used on the web, whereby the origin point lies at the top-left corner of the page, distances are given from the left of something or from the top of something.

Technically, though this doesn't mean that we can't work out the distance of an element from the right of something or from the bottom of something; it's just a little bit of extra work.

Anyways, consider the following example:

<div id="d1">A simple div</div>
body {
   margin: 10px;
   padding: 5px;
}

#d1 {
   font-size: 30px;
   background-color: lightgrey
}
A simple div

The <body> has a margin of 10px and a padding of 5px. Given this info, what do you think is the offset of the <div> element?

Well, it's really simple. First, we gotta appreciate the fact that the offset is customarily given in two different directions, i.e. the horizontal distance from the left edge of something and the vertical distance from the top edge of something.

Following this notion, we see that the offset of the <div> element from the top edge of the document is 15px and so is the case for its offset from the left edge of the document, i.e. its also 15px.

Not really that difficult, was it?

Sometimes, we can refer to the offset of an element from the top edge of something simply as its top offset. The same thing applies to the offset from the left edge of the viewport, i.e. it can be referred to as the left offset.

Coming back to the properties offsetLeft and offsetTop, they both return the offset of an element from the left and top edge of its nearest positioned element, respectively.

And now it's time to talk about the term 'nearest positioned ancestor'.

For a given element, its nearest positioned ancestor is simply the ancestor element that is closest to it, in terms of the whole tree structure, and that meets either of the following criteria:

  • Isn't statically positioned, i.e doesn't have the default position: static CSS style applied.
  • Is statically positioned, but is a <td>, <th> or a <table> element.
  • Is the <body> element.

From this, note that the <body> element or the root <html> element, both don't have a nearest positioned element. Also, if an element has display: none set on it or position: fixed, it doesn't have a nearest positioned element either.

The good news for us in this regard is that for any given element, we can get to know of its nearest positioned element via the property offsetParent.

As stated before, offsetParent isn't bound to return the actual parent of a given DOM element node. Rather, it's bound to return the ancestor of the given element that meets the criteria of the nearest positioned ancestor as mentioned above.

Frankly speaking, a better name for offsetParent would've been offsetAncestor. What do you think?

As per the CSSOM View Module specification, the computation of offsetParent for a given element can be found right at the start of section 7: Extensions to the HTMLElement Interface.

If an element doesn't have a nearest positoned ancestor, offsetParent returns back null.

Let's take the help of a decent amount of examples to make intuition of all this lengthy, and somewhat boring, discussion and make it a little bit more interesting.

First, let's get the hang of offsetParent and, obviously, of the nearest positioned ancestor with it.

Consider the following example that we saw above:

<div id="d1">A simple div</div>
body {
   margin: 10px;
   padding: 5px;
}

#d1 {
   font-size: 30px;
   background-color: lightgrey
}
A simple div

Let's inspect the offsetParent of the <div> element:

document.querySelector('#d1').offsetParent.nodeName
'BODY'

And, it's the <body> element. The very first ancestor of <div> here is <body>, and so without further ado, offsetParent becomes <body>. This one was very very simple.

Let's consider another example.

Everything below is the same as before, just this time we've encapsulated the <div> element around a <section>:

<section>
   <div id="d1">A simple div</div>
</section>
body {
   margin: 10px;
   padding: 5px;
}

section {
   border: 5px solid red;
   padding: 10px
}

#d1 {
   font-size: 30px;
   background-color: lightgrey
}
A simple div

Can you figure out the offsetParent of the <div> element here?

document.querySelector('#d1').offsetParent.nodeName
'BODY'

Once again, the offsetParent is <body>. The first ancestor of <div> here is <section>, but it's statically positioned (and isn't either of the elements <td>, <th> or <table>). Hence, we consider the next ancestor up the chain and see that it's <body>. Likewise, offsetParent becomes <body>.

Let's look at yet another example. The HTML below is the exact same as the last example above, with the exception of the fact that the <section> element is now made relatively positoned:

<section style="position: relative">
   <div id="d1">A simple div</div>
</section>

Now, if we inspect the offsetParent of <div>, we expect to see the <section> element returned. Let's try it out:

document.querySelector('#d1').offsetParent.nodeName
'SECTION'

As expected, the offsetParent is the <section> element by virtue of the fact that <section> has position: relative set on it.

In the code below, we quickly inspect the offsetParent of a couple of elements on the document:

<div id="d1">
   <div id="d2">A simple div</div>
</div>
body {
   margin: 10px;
   padding: 5px;
}

#d1 {
   position: fixed;
   width: 80%;
   background-color: yellow;
}

#d2 {
   font-size: 35px;
   border: 5px solid red;
   margin: 15px
}

Live Example

Following is the console snippet where we inspect the offsetParent property on each element up the chain starting with the innermost <#d2> element:

document.querySelector('#d2').offsetParent.id
'd2'
document.querySelector('#d1').offsetParent
null
document.body.offsetParent
null
document.documentElement.offsetParent
null

The return value of each statement is quite simple to understand:

  • The first ancestor of #d2, that is #d1, has position: fixed set on it, hence the offsetParent of #d1 becomes the #d2 element.
  • Since #d2 has position: fixed set on it, it doesn't have a nearest positioned ancestor, i.e. its offsetParent is null.
  • As stated above, both <body> and <html> don't have a nearest positioned ancestor and likewise their offsetParent is null.

Hopefully, at this stage, you are comfortable with the idea of offsetParent and so we can finally move over to consider the properties offsetLeft and offsetTop.

To state it once again, and this time more precisely, offsetTop returns back the distance of an element's border box from the top edge of its offsetParent's padding box. A similar reasoning applies to offsetLeft.

However, keep in mind, that if a given element's offsetParent is null, this doesn't necessarily mean that its offsetTop or offsetLeft would be 0.

As specified in the algorithm for offsetTop (and a similar one for offsetLeft), if the offsetParent is null, further computation is done before calculating the final value of offsetTop for a given element.

This is detailed as follows:

  • If the element is <body> or has display: none set on it (both of whose offsetParent is null), offsetTop is 0.
  • Otherwise, if the offsetParent of the element is null because of some other reason (for e.g. position: fixed), return the element's distance from the top edge of the web page.

Let's consider a quick example:

In the code below, we inspect the offsetTop of the <div> element:

<section>
   <div id="d1">A simple div</div>
</section>
body {
   margin: 10px;
   padding: 5px;
}

section {
   border: 5px solid red;
   padding: 30px;
   position: relative
}

#d1 {
   font-size: 30px;
   background-color: lightgrey
}
A simple div

As we can clearly see, the <section> element has a large padding of 30px while a border of 5px on all sides. Our job is to get the value of offsetTop of the <div> element. What do you think it would be equal to?

Well, let's see it:

document.querySelector('#d1').offsetTop
30

Just as expected, offsetTop is 30 as the padding around the <section> element, which is the offsetParent of the <div>, is 30px.

The figure below illustrates what dimension does offsetTop exactly target in the example above:

An illustration showing what dimensions offsetTop targets in the example above.
An illustration showing what dimension offsetTop targets in the example above.

Simple?

Let's consider removing the position: relative declaration from the <section> element above, and then re-inspect the offsetTop of the <div>:

document.querySelector('section').style.position = 'static'
'static'
document.querySelector('#d1').offsetTop
50

So this time the value of offsetTop gets promoted to 50. But how?

Well, first let's determine the offsetParent of the <div>, which is extremely simple. It's just the <body> element. So the <div>'s offsetTop is just the distance between the top edge of its border box and the top edge of <body>'s padding box. This should technically be equal to 40, however the value above is 50.

Where does the extra 10px come from?

Well, this is an edge case.

That is, when the offsetParent is <body> and it's statically positioned, as in the example above, its margin box is considered to be its padding box. And so this means that the padding box of <body> above includes its 10px margin as well.

The figure below illustrates the exact dimension that offsetTop targets in this example:

An illustration showing what dimension offsetTop targets in this example.
An illustration showing what dimension offsetTop targets in this example.

If we apply position: relative to <body>, its padding box would work as usual, i.e. it won't include its margins as well.

In the following snippet, we do this and then re-inspect the offsetTop of the <div>:

document.querySelector('section').style.position = 'static'
'static'
document.querySelector('#d1').offsetTop
50

Apparently, now the extra 10px from the margins around <body> doesn't get counted, all thanks to the application of position: relative to it.

The figure below illustrates offsetTop in this very example:

An illustration showing what dimension offsetTop targets in this example.
An illustration showing what dimension offsetTop targets in this example.

Dimensions of the border box

To retrieve the width of the border box of an element, we can use its offsetWidth property. Similarly, to retrieve its height, we can use the offsetHeight property.

To recap it, the border box of an element is the smallest box around it that includes all of its borders. For instance, consider the figure below:

The border box for an element.
The border box for an element.

The element has 10px of border on all but the bottom side, a padding of 20px, an absolute width of 500px, and an absolute height of 100px. Based on the dimensions given above, it's quite easy to calculate the width and height of the border box:

Width: 500 + 20 + 20 + 10 + 10 = 560px
Height: 100 + 20 + 20 + 10 = 150px

Note that offsetWidth and offsetHeight, just like offsetLeft and offsetTop, don't consider any CSS transformations applied to the underlying element.

Let's now consider an example.

In the code below, we have a <div> element that tries to replicate the figure shown above:

<div id="d1">A div</div>
#d1 {
   border: 10px solid #777;
   border-bottom: none;
   padding: 20px;
   width: 500px;
   height: 100px;
}
A div

As we calculated the width and height of the border box above, they were 560px and 150px, respectively. Let's see what does offsetWidth and offsetHeight have to offer us:

var divElement = document.querySelector('#d1')
undefined
divElement.offsetWidth
560
divElement.offsetHeight
150

As expected, both the properties yield the same values that we calculated manually.

Let's consider this same example, just this time with a CSS transformation applied to it additionally:

<div id="d1">A div</div>
#d1 {
   border: 10px solid #777;
   border-bottom: none;
   padding: 20px;
   width: 500px;
   height: 100px;
transform: scale(0.5); }
A div

As mentioned before, offsetWidth and offsetHeight don't take CSS transformations into account, hence both properties should expectedly return back the same values as they did before.

divElement.offsetWidth
560
divElement.offsetHeight
150

And yes they do!

Recall that to obtain the exact dimensions of the border box of an element after applying CSS transformations we can use the getBoundingClientRect() method. This can be seen as follows:

divElement.getBoundingClientRect().width
280
divElement.getBoundingClientRect().height
75

As can be seen, each dimension is half its corresponding value when inspected via offsetWidth and offsetHeight, respectively.

The edge case of the <html> element

The border box of an element always includes any scrollbars associated with it. This doesn't however apply to the <html> element. When we apply borders to the <html> element, they don't encapsulate the scrollbar of the document. Hence, the border box of <html> doesn't include its scrollbar.

Remember that the scrollbar applied to a web page by default is applied to the <html> element, not to the <body> element.

Now what this means for us is that the offsetWidth of <body> excludes the width of the scrollbar of the web page, simply because the scroll bar isn't part of <body>. (Note that if we somehow change the styles of the web page such that the scrollbar is applied over the <body> element, then this might change.)

Moreover, this also means that the offsetWidth of <html> excludes the width of the scrollbar of the web page as well.

The clientLeft and clientTop properties

Named akin to offsetLeft and offsetTop, we have the properties clientLeft and clientTop available on element nodes.

The question is: what's their purpose?

Well, clientTop returns the distance of the padding box from the top edge of the border box of an element. In other words, this is just the width of the top border plus the height of the top scrollbar (if there is any scrollbar).

The same goes for the clientLeft. Stating it precisely, clientLeft is simply the width of the left border of the underlying element plus the width of its left scrollbar (if there is any scrollbar).

Beware of the word 'client' here!

As we mentioned at the very start of this chapter, the word 'client' here is misleading.

In other parts of the CSSOM API, such as in the method getBoundingClientRect(), 'client' refers to the browser window. However, in clientTop, 'client' refers to the padding box of the element under inspection.

The figure below illustrates the clientTop property.

An illustration demonstrating the clientLeft and clientTop properties.
An illustration demonstrating the clientLeft and clientTop properties.

Let's consider an example:

<div id="d1">A div</div>
#d1 {
   border: 20px solid #bbb;
   padding: 20px;
}
A div
var divElement = document.querySelector('#d1')
undefined
divElement.clientLeft
20
divElement.clientTop
20

As is evident, the width of the left and the top borders of the <div> element is 20px, and likewise clientLeft and clientTop both yield 20.

Dimensions of the padding box

Where offsetWidth and offsetHeight apply to the dimensions of the border box of an element, the clientWidth and clientHeight properties apply to its border box.

In particular, clientWidth returns back the width of the calling element's padding box excluding any rendered scrollbars, and obviously also ignoring any applied CSS transformations. The same goes for clientHeight, albeit for the height of the element's padding box.

To recap it, the padding box of an element is the smallest box around it that includes all of its padding. This excludes any rendered scrollbars. Consider the figure below.

The padding box for an element
The padding box for an element

Let's try to calculate the width and height of the padding box here:

Width: 500 + 20 + 20 = 540px
Height: 100 + 20 + 20 = 140px

Easy, isn't this?

As we did with offsetWidth above, let's try to replicate this figure using an actual HTML element and then retrieve its clientWidth and clientHeight.

<div id="d1">A div</div>
#d1 {
   border: 10px solid #bbb;
   padding: 20px;
}
A div
var divElement = document.querySelector('#d1')
undefined
divElement.clientWidth
540
divElement.clientHeight
140

The scrollLeft and scrollTop properties

Recall from the CSSOM Viewport chapter, the global scrollX and scrollY properties tell us about the scroll position of the HTML document in the given viewport

A similar kind of information can be obtained for an element as well in the CSSOM API, thanks to the scrollLeft and scrollTop properties of the Element interface, respectively.

The scrollLeft property represents the horizontal scroll position of a given element, supposing that it has an associated scrollbar.

Unlike the global scrollX property, however, it's both capable of being read from and written to. As you can probably guess, when scrollLeft is set to a particular value, the underlying element is horizontally scrolled to that very position.

On the same lines, the scrollTop property, which is arguably more commonly used than scrollLeft due to the natural flow of content on most web pages, represents the vertical scroll position of a given element.

Now here are certain things to note regarding setting scrollLeft and scrollTop:

  1. If the element doesn't have a scrollbar, setting either scrollLeft or scrollTop has no effect.
  2. Setting either property to a value greater than the maximum possible scroll position in the respective direction gets it to be set to that maximum value instead.
  3. Setting either property to a negative value gets it to be set to 0 instead.

As always, examples are the best teachers, ain't they? Likewise, let's consider a couple of them to better understand the behavior of scrollTop.

Consider the following code where we have a <section> element with a lot of content inside it and with the overflow: scroll style set so that it's scrollable:

<section id="s1">
   This is a section with a lot of content. The purpose of so much content is just so that the element is scrollable.
   <br><br>
   <div id="d1">A small div</div>
   <br><br>
   Lorem ipsum dolor sit amet consectetur adipisicing elit. Provident vero magnam, ipsa itaque at omnis amet nisi cumque error harum maiores possimus quasi officia ea architecto obcaecati incidunt consequuntur ex!
</section>
<br>
<button>Get scrollTop</button>
#s1 {
   overflow: scroll;
   padding: 20px;
   border: 5px solid #777;
   height: 100px
}

#d1 {
   height: 200px;
   width: 50%;
   background-color: yellow;
}
This is a section with a lot of content. The purpose of so much content is just so that the element is scrollable.

A small div


Lorem ipsum dolor sit amet consectetur adipisicing elit. Provident vero magnam, ipsa itaque at omnis amet nisi cumque error harum maiores possimus quasi officia ea architecto obcaecati incidunt consequuntur ex!

Our aim is to alert the current vertical scroll position of the <section> element when the given button is clicked. This is accomplished below:

var buttonElement = document.querySelector('button');
var sectionElement = document.querySelector('#s1');

buttonElement.onclick = function() {
   alert(`scrollTop: ${sectionElement.scrollTop}px`);
}

Live Example

This example demonstrated the scrollTop property in the read context. Now, let's see it in the write context.

Consider the following code where we have the same HTML and CSS setup as before, just this time the button's click handler is configured to vertically scroll the <section> by a further 10px:

var buttonElement = document.querySelector('button');
var sectionElement = document.querySelector('#s1');

buttonElement.onclick = function() {
   // Vertically scroll the section by a further 10px.
   sectionElement.scrollTop += 10;
}

Live Example

The statement sectionElement.scrollTop += 10 is effectively the same as the statement sectionElement.scrollBy(0, 10).

So this was simple, what do you think?

Moving on, there is an important thing to take note of while working with scrollLeft or scrollTop (or any other mechanism of scrolling an element, such as scroll(), scrollBy() as we shall see later on).

That is, changing the scroll position of an element works regardless of the fact if the element has a scrollbar on it or not. But keep in mind that this assumes that the element has content overflowing it, which might either be hidden (via overflow: hidden) or made accessible by means of scrolling (via overflow: auto or overflow: scroll).

For an element's scroll position to be changed, it should sensibly be that the element has overflowing content. Otherwise, there is absolutely no point and no way of changing the scroll position — it would remain at the zero position!

We've already witnessed above that when a given element has a scrollbar rendered alongside it, changing its scroll position by means of setting scrollLeft and/or scrollTop does have an effect.

However, in the following example, the exact same as before, we also demonstrate this for the case when the element doesn't have a scrollbar rendered alongside it (but still has overflowing content) by virtue of the style overflow: hidden:

#s1 {
   overflow: hidden;
}
This is a section with a lot of content. The purpose of so much content is just so that the element is scrollable.

A small div


Lorem ipsum dolor sit amet consectetur adipisicing elit. Provident vero magnam, ipsa itaque at omnis amet nisi cumque error harum maiores possimus quasi officia ea architecto obcaecati incidunt consequuntur ex!

Live Example

Open the link above and try clicking the button again and again. As you do so, you'll notice it producing the exact same effect as in the last example above.

Stating it once again, if an element does or doesn't have a scrollbar rendered alongside it, it still can be scrolled programmatically, but this assumes that the element has content overflowing it. Simply put, if there is no overflowing content, there can be no scrolling, at all!

Dimensions of the content

Before we talk about the scrollWidth and scrollHeight properties, let's first quickly recap offsetWidth/offsetHeight and clientWidth/clientHeight.

  • offsetWidth/offsetHeight target the width and height of the border box of the underlying element, respectively.
  • clientWidth/clientHeight target the width and height of the padding box of the underlying element, respectively.

Alright, so now what exactly is scrollWidth and scrollHeight?

Well, in simple terms, scrollWidth is the width of the content of a given element.

Note that this might be different from the width of the content box of the element — the content might overflow out of the content box and thus have a different dimension than that of the content box.

Consider the HTML + CSS below:

<section id="s1">
   <div id="d1">A simple div. Lorem ipsum dolor sit amet consectetur adipisicing elit. Ab tempora quia doloribus esse laborum quis voluptatum excepturi nobis et ullam, eaque repellendus suscipit, consequuntur maxime quisquam deserunt saepe. Nam, ea.<div>
</section>
#s1 {
   overflow: auto;
   height: 100px;
   width: 300px;
   padding: 20px;
   background-color: #eee
}

#d1 {
   /* The following width and height make #d1 to overflow #s1 */
   height: 200px;
   width: 600px;
   background-color: yellow;
}

Here's the output this produces:

A simple div. Lorem ipsum dolor sit amet consectetur adipisicing elit. Ab tempora quia doloribus esse laborum quis voluptatum excepturi nobis et ullam, eaque repellendus suscipit, consequuntur maxime quisquam deserunt saepe. Nam, ea.

The <section> element has a <div> that's larger both in width and height than it and thus has a scrollbar rendered alongside it, courtesy of the overflow: auto style.

Based on the code above, the figure below illustrates scrollWidth of <section>:

An illustration demonstrating scrollWidth.

As can be seen, the scrollWidth of the <section> element is the width of the bounding box around it that surrounds all of its content, including the one that overflows it.

Keep in mind that, just like clientWidth:

  • scrollWidth doesn't consider the width of the borders on either side of the given element.
  • scrollWidth doesn't consider the width of the scrollbar (this is obvious because it measures the width of the content behind the scrollbar).
  • scrollWidth does consider the padding on both sides of the given element.

The same concepts can be applied to the scrollHeight property, but for the vertical dimension of the element rather than its horizontal dimension.

Now, following from this discussion, there is an extremely ingenious approach that we could take to determine whether there is content overflowing a given element or not. This is detailed as follows:

Determining if an element has overflowing content

If the scrollWidth/scrollHeight of a given element is larger than its clientWidth/clientHeight, this simply means that the element has overflowing content.

Note that by no means does this tell us that the element has a rendered scrollbar — this can only be confirmed by inspecting the CSS styles of the element which requires us to invoke the global getComputedStyle() function, passing the element as an argument.

We'll see how to work with getComputedStyle() later in the CSSOM Computed Styles chapter.

Coming back to the example above, let's actually inspect the value of the scrollWidth property of the <section> element:

var sectionElement = document.querySelector('#s1');

console.log('scrollWidth', sectionElement.scrollWidth);
console.log('scrollHeight', sectionElement.scrollHeight);
scrollWidth 640 scrollHeight 240

Each of the values here makes perfect sense:

  • scrollWidth is 640 because the width of the <div> is 600px and the padding on both ends of <section> is 20px each.
  • scrollHeight is 240 because the height of the <div> is 200px and the padding on both ends of <section> is 20px each.

The scrollIntoView() method

Recall the example from the CSSOM Viewport chapter where we used a button and a click handler on it to scroll the viewport such that a given element's top edge aligns with the viewport's top edge.

However, in that chapter, we manually calculated and hardcoded the distance of the element from the top edge of the document, which was passed in to the scroll() global method.

Here's that example reviewed:

<button>Bring heading into view</button>
<h1>This is a heading</h1>
body {
   margin: 0;
   height: 2000px;
}
button {
   position: absolute;
   top: 0;
   padding: 15px;
}
h1 {
   margin-top: 1000px;
   background-color: yellow;
   height: 100px;
}
var buttonElement = document.querySelector('button');

buttonElement.onclick = function() {
   // Bring the <h1> element into view by scrolling the viewport
   // 1000px vertically.
   scroll(0, 1000);
}

Live Example

A slightly better option would be to programmatically compute the distance of the element from the top of the page, in JavaScript, and then scroll the viewport to that particular position. Everything else remains the same.

We accomplish this in the following code:

var buttonElement = document.querySelector('button');
var h1Element = document.querySelector('h1');

buttonElement.onclick = function() {
   // The distance of <h1> from the top edge of the document.
   var topOffset = h1Element.getBoundingClientRect().top + scrollY;
   scroll(0, topOffset);
}

Live Example

The variable topOffset represents the distance of <h1> from the top of the document. It's computation is quite trivial:

  • The getBoundingClientRect() method returns its distance from the top edge of the viewport.
  • scrollY gives the current vertical scroll position of the document. This is added to the previous value just in case the viewport was automatically scrolled by the browser before the execution of getBoundingClientRect().
Getting an element's distance from the top of the document by adding scrollY (or its alias pageXOffset) is safe play. Sometimes, browsers automatically scroll a document to a given position and if that happens, then solely relying on getBoundingClientRect().top would only end up with the wrong distance — we'd need to add scrollY to it as well.

As you might agree, this second approach is definitely better and more flexible than the last one above. We can position the <h1> element anywhere and be rest assured that upon the button's click, the viewport gets scrolled such that the element comes into view, just as desired.

However, there still is an even more flexible and better option and that is to use the Element interface's scrollIntoView() method to accomplish essentially the same thing, although scrollIntoView() is much more sophisticated and complex in its inner workings.

Talking about the syntax of scrollIntoView(), it has three different (overloaded) forms:

The first one shown below takes no arguments and simply scrolls the viewport to align the element's top edge with the viewport's top edge:

scrollIntoView()

The second one takes a single Boolean argument as follows:

scrollIntoView(alignToTop)

alignToTop specifies whether we want the element's top edge to align with the top edge of the viewport or not. If it's true, the top edges are aligned together. Otherwise, the bottom edges of the element and the viewport are aligned together.

The third one takes a single object literal as argument, allowing us to specify a number of various configurations:

scrollIntoView(options)

options can take the following properties:

  1. block specifies the vertical position on the viewport where the element must show up. Can be either 'start', 'center', 'end' or 'nearest'. The default is 'start'.
  2. inline — specifies the horizontal position on the viewport where the element must show up. Can be either 'start', 'center', 'end' or 'nearest'. The default is 'nearest'.
  3. behavior — same as the behavior property of the object passed into the global scroll(), scrollTo() and scrollBy() methods. The default value is 'auto'.

The block property configures scrolling in the vertical (i.e. block) direction and works when the given element has a vertical scrollbar. Similarly, the inline property configures scolling in the horizontal (i.e. inline) direction and works when the element has a horizontal scrollbar.

As for the values of both of these properties, they're also really simple to understand.

'start' gets the element's starting edge to coincide with the starting edge of the viewport. The 'starting' edge might be different depending on the property we're talking about:

  • If we are considering the block property, 'start' relates to the vertical axis and thus, gets the element's top edge (which is the starting edge vertically) to coincide with the viewport's top edge.
  • If we are considering the inline property, 'start' similarly relates to the horizontal axis and thus, gets the element's left edge (which is the starting edge horizontally) to coincide with the viewport's left edge.

A similar reasoning can be applied to the values 'center' and 'end'.

'nearest' is a special case. It is somewhere in between 'start' and 'end'.

In particular, if the element (on which scrollIntoView() is invoked) is closer to the 'start' position, 'nearest' is equivalent to 'start'. On the same lines, if the element is closer to the 'end' position, 'nearest' is equivalent to 'end'.

The 'nearest' value for the properties block and inline isn't supported on many browsers at the time of this writing.

Simple?

Alright, it's time for some examples.

In the following code, we use scrollIntoView() to bring the <h1> element, shown in the first example above, into view:

var buttonElement = document.querySelector('button');
var h1Element = document.querySelector('h1');

buttonElement.onclick = function() {
   h1Element.scrollIntoView();
}

Live Example

To demonstrate the other forms of scrollIntoView(), extending this same example, below we pass in false as an argument in order to get the bottom edges of the <h1> element and the viewport to coincide with each other:

var buttonElement = document.querySelector('button');
var h1Element = document.querySelector('h1');

buttonElement.onclick = function() {
   h1Element.scrollIntoView(false);
}

Live Example

And now, let's test the third form of the method:

var buttonElement = document.querySelector('button');
var h1Element = document.querySelector('h1');

buttonElement.onclick = function() {
   // Smoothly bring the element into view and centralize
   // it on the viewport.
   h1Element.scrollIntoView({
      behavior: 'smooth',
      block: 'center'
   });
}

Live Example

Note that sometimes, based on the dimensions of the document or other ancestor elements, it's not entirely possible to align a particular edge of an element with the same edge on the viewport.

For instance, in the following example, the <body> element has no absolute height and, therefore, no scrollbar while the <h1> element is just 100px away from the top edge of the document:

<button>Bring heading into view</button>
<h1>This is a heading</h1>
body {
   margin: 0;
}
button {
   position: absolute;
   top: 0;
   padding: 15px;
}
h1 {
   margin-top: 100px;
   background-color: yellow;
   height: 100px;
}
var buttonElement = document.querySelector('button');
var h1Element = document.querySelector('h1');

buttonElement.onclick = function() {
   h1Element.scrollIntoView();
}

Live Example

Now, if we try to click the button, the scrollIntoView() method would still be invoked but there would be no way for the document to be scrolled even by 1px and therefore no way to get the top edges of the <h1> and the viewport to be aligned with one another.

"I created Codeguage to save you from falling into the same learning conundrums that I fell into."

— Bilal Adnan, Founder of Codeguage