Introduction

Scroll events are quite popular in JavaScript applications since they are directly tied with many features that include layout changes, lazy loading images, infinite scrolling and so on.

Even the simplest of webpages will have some sort of scroll listeners working behind the scenes to power these very types of features.

In this chapter we will explore all the details to the scroll event in JavaScript and along with it all the functionalities that can be used along with it.

These include certain properties that can give us the scroll position of the webpage as well as a given element; and certain methods that can help us in changing the scroll offset of anything programmatically!

After getting yourself familiar with all the concepts taught here, head over to challenge yourself at JavaScript Scroll Event Quiz.

In short, there's a lot to cover - so let's begin!

The event

Each time an element is scrolled, a scroll event is fired on it.

Subsequent actions can, obviously, be then made using the onscroll handler:

targetObject.onscroll = function() {
    // handling code
}

..or even by listening for the scroll event using addEventListener():

targetObject.addEventListener("scroll", function() {
    // handling code
});
To learn more about event handlers and the addEventListener() method, refer to JavaScript Event Handlers and JavaScript addEventListener() method.

But before we can handle the scroll event, we first need to know all the ways in which scrolling can be done.

A given element, if it's scrollable, can be scrolled in a handful of ways as highlighted below:

  1. Using the scrollbar manually
  2. Using the mouse (arrow buttons on the scroll bar, or via the mouse wheel)
  3. Using the keyboard, in particular the arrow, page down, page up keys etc.
  4. Using touch gestures
  5. Using JavaScript
  6. By visiting an ID link

In some browsers, when the scrollbar is used to perform the scrolling, it often makes 2px or 3px increments; which means that even if we drag the scrollbar to the shortest of lengths, a 2px change would incur in the scrolled length, commonly referred to as the scroll offset.

The amount by which an element is scrolled from its initial position is known as its scroll offset.

The same goes for scrolling done using keys.

It's usually the touch event that enables a user to scroll a webpage at precise 1px increments - all other methods tend to make larger increments.

The JavaScript functionalities used to change the scroll offset, of a webpage or an element, are the properties scrollTop and scrollLeft, as well as the methods scrollTo() and scroll().

One interesting case where scrolling gets performed, listed as the last option above, is when we visit a link referring to an ID, in the HTML document.

Such anchor links have an href attribute that starts with a hash # symbol, for example #article1. Visiting them takes us to the element with the given id. In this case, it will take us to the element with id="article1".

Often, when visiting an ID link, the respective element's top edge is aligned with the top edge of the viewport.

With the ways of performing a scroll understood, let's now consider a quick example of handling the scroll event.

Below we create a <div> element and assign it some CSS styles in order to make it scrollable:

<div><p>Some content</p></div>
For more info on how to make an element scrollable, consider reading CSS Overflows.
div {
    height: 200px;
    overflow: auto;
    background-color: #ddd
}
p {
    height: 500px;
    /* height exceeds 200px so a scrollbar will be given */
}

Next we assign a function to the onscroll property of the div element to respond to its scrolling.

div.onscroll = function(e) {
    console.log("Scrolling");
}

Handling the scroll event can also be done using the addEventListener() method, as shown below:

div.addEventListener("scroll", function(e) {
    console.log("Scrolling");
})

With all this is place, we get the following result:

Some content
Open the console to view the logs made by scrolling this div.
Learn AJAX

Scrolling the document

The most common target of the onscroll handler is the window object i.e the whole HTML document.

It's common because usually many subroutines of a web application are directly tied with the scroll offset of the document, such as monitoring ad impressions, or lazy loading images and so on.

Following we illustrate scroll handling, as done on the window object:

window.onscroll = function(e) {
    console.log("Scrolling");
}

Go on and try to scroll the document; you'll simply see logs filling up the console.

Note that in place of window we can also use document.body, as both of them refer to the same onscroll handler:

window.onscroll = function(e) {
    console.log("Scrolling");
}

console.log(window.onscroll === document.body.onscroll); // true

document.body.onscroll = null;
console.log(window.onscroll); // null

You give an onscroll handler to document.body, it will be applied to window; you give it to window, it will be applied to document.body.

They both are simply interchangeable!

However, for simplicity, developers often use window - come on, it's seven characters shorter!

Moving on, what we will do now is make our onscroll handler more fruitful by retrieving the scroll offset, in units of pixels.

Tracking the scroll offset

In regards to this, JavaScript provides us with four global window properties that hold the scroll offset of the document.

Two of them are fairly recent, while the other two are from the old ages.

First, for the newer set, we have the properties scrollX and scrollY, and for the other set, we have pageXOffset and pageYOffset.

As the names suggest, scrollX and pageXOffset return the horizontal scroll offset whereas scrollY and pageYOffset return the vertical scroll offset, all as numbers.

Recall that scroll offset is the amount, in pixels, by which a given element is scrolled past its initial position.

Using these properties, we can track the scroll offset of our webpage and consequently power numerous features that rely on this idea.

Consider the code below:

window.onscroll = function(e) {
    // log the length scrolled vertically
    console.log(window.pageYOffset);
}

Rather than making fixed logs, as we did in the previous example, here we log the current scroll offset of the document, which is a common concern in almost all scroll handlers.

Live Example

To get an understanding of the significance of being able to track the scroll offset of the document, consider the following task.

Write some code to make an alert "Into view" the moment the div element shown below comes into view.

<!--some content here-->
<div style="background-color: #ddd">Good to go!</div>

Once it does come into view, remove or nullify the scroll listener/handler, whichever you've used.

The div element is 327px far away from the bottom of the viewport, and likewise, from coming into view.

First with the onscroll handler:

window.onscroll = function() {
    if (window.pageYOffset >= 327) {
        window.onscroll = null;
        alert("Into view");
    }
}

Now with the scroll listener:

function scrollListener() {
    if (window.pageYOffset >= 327) {
        window.removeEventLisener("scroll", scrollListener);
        alert("Into view");
    }
}

window.addEventListener("scroll", scrollListener);

Construct a scroll handler for window that fixes the #tab element shown below, once its top edge touches the top of the viewport.

<div id="tab-cont">
    <!--Fix the following #tab div-->
    <div id="tab">Tab</div>
</div>

Following are a couple of CSS styles set up:

#tab {
    background-color: #bbb;
    box-sizing: border-box;
    padding: 10px;
}
.fixed {
    position: fixed;
    top: 0;
    width: 100%
}
Read more about position: fixed at CSS Positioning.

To fix #tab, give it the class fixed; and likewise to unfix it, remove the class.

Before you fix #tab, make sure that you give a custom height to its parent because on fixing #tab, its height will reduce down to 0 and thus cause the content below it to get a jump shift!

You shall use a JavaScript method to calculate #tab's height. Try to figure out that method!

Use classList for the class addition/removal logic.

Following is the solution:

var tab = document.getElementById("tab");
var tabRect = tab.getBoundingClientRect();
var tabOffsetTop = tabRect.top + window.pageYOffset;

// get #tab's height and give it to #tab-cont
tab.parentNode.style.height = tabRect.height + "px";

window.onscroll = function() {
    if (window.pageYOffset >= tabOffsetTop) {
        tab.classList.add("fixed");
    } else {
        tab.classList.remove("fixed");
    }
}

Live Example

First we retrieve the #tab element and then compute its offset from the top of the document (in line 3, saved in tabOffsetTop).

After this, we give its parent a height value equal to its own height (in line 6).

Finally we handle the scroll event on window - here we check whether pageYOffset is greater than or equal to tabOffsetTop and if it is then give it the class fixed.

If it's not, then we remove this class so that #tab can return back to its original position.

Scrolling an element

Similar to the window object, we can attach a scroll handler to any HTML element we like (obviously excluding tags in <head>).

Everything remains the same except for the properties to track the scroll offset.

For window we use pageXOffset and pageYOffset (or interchangeably scrollX and scrollY), but for elements, the case is different.

For HTML elements, we use the properties scrollTop and scrollLeft.

Below we create a div element, give it a couple of CSS styles, and then finally handle its scroll event, where we log its scroll offsets:

<div id="area">
    <p>Some content inside this div.</p>
</div>
#area {
    height: 300px;
    width: 80%;
    overflow: auto;
}
p {
    height: 600px;
    width: 120%;
}
These styles are given solely to make the element #area scrollable.
For more info on how to make an element scrollable, consider reading CSS Overflows.
var area = document.getElementById("area");
area.onscroll = function() {
    console.log(this.scrollLeft, this.scrollTop);
}

Live Example

In the link given above, instead of logging scrollLeft and scrollTop, we display them inside two <span> elements so that you can visualise their values without a trip to the console.

The scrollLeft and scrollTop properties both have one very interesting feature that can be used to change the scroll offset of an element.

That is, they allow values to be assigned to them to point to the given scroll position.

Let's discuss on this...

Changing the scroll offset

If we assign a value to the scrollLeft or scrollTop property, the element on which we call the property will be scrolled to the provided value.

For example, if we want to navigate the div element above to a 50px vertical scroll offset, we can simply assign the number 50 to the scrollTop property as shown below:

<div id="area">
    <p>Some content inside this div.</p>
</div>
The CSS for this example is the same as the one shown above.
var area = document.getElementById("area");
area.scrollTop = 50;

Live Example

In the link given above, we execute the statement div.scrollTop = 50 on the click of a button, so that you can easily visualise the change!

The same idea can also be applied to the main HTML document, however with certain wierd things.

As we said above, window.onscroll is analogous to document.body.onscroll i.e both point to the same function object.

Likewise, a novice developer would naturally think that to navigate the document to a given scroll offset he could simply call document.body.scrollTop = scrollOffset - but strangely this doesn't work!

This is because the scrollbar the browser renders for the document is applied on the <html> element, NOT on the <body> element.

The scrollTop property can only navigate the caller object's own scrollbar to the provided value - if it ain't has any scrollbar then the assignment won't simply work.

Now when someone tries to make intuition of this, somehow, another strange thing arises!

Handling the scroll event on the documentElement object has no effect. As we scroll the document, the respective handler isn't fired, despite the fact that the scrollbar is actually applied on the documentElement object.

So what's best to do in this case is to take this as some sort of bug in the scroll behaviour of browsers, and tackle with it patiently. How to tackle with it?

Well luckily it's not difficult!

Methods to scroll

Scrolling the window object in JavaScript is quite strange if done using the scrollLeft and scrollTop properties.

Likewise, we are provided with three methods to ease in changing the scroll offset of window, and of any element we wish: scroll(), scrollTo() and scrollBy().

Following is a discussion on these methods.

scroll() and scrollTo()

The method scroll() is available on both Window and Element objects and serves to scroll its caller object to given offset co-ordinates.

Below shown is the general form of the method as called with two arguments:

WindowOrElementObject.scroll(x, y)
  1. x is the horizontal scroll offset where we want to go.
  2. y is the vertical scroll offset where we want to go.

The method can also be provided a ScrollToOptions object with three properties.

WindowOrElementObject.scroll(options)
  1. left: the horizontal scroll offset
  2. top: the vertical scroll offset
  3. behavior: determines how the scrolling should be done i.e whether it should be performed smoothly - "smooth" or in the blink of an eye - "auto". By default, it's set to "auto".
Remember that the behavior property has no 'u' in it i.e it's spelled as 'behavior' and NOT as 'behaviour'!

Consider the following code utilising the first case:

window.scroll(50, 300);

Here we provide two arguments to the method scroll(), as called on window, which will result in the document being precisely taken to that scroll offset.

Live Example

Now over to the second case:

window.scroll({
    left: 50,
    top: 300,
    behavior: "smooth"
});

This time we provide an object with the same scroll offsets as before, with the addition of the behavior property. Set to "smooth", it will cause the scroll transition to be made smoothly.

Live Example

There is another method scrollTo() that's exactly the same as scroll(). Use one or the other, it's upto you!

Likewise we'll skip the explanation for scrollTo() and instead move to the third and last method to explore i.e scrollBy().

scrollBy()

Often times, instead of scrolling to an absolute value as the method scroll() (and even scrollTo()) does, we need to scroll by a given value.

For instance, if initially at a scroll offset of 100px, scrolling 'to' 200px would change the scroll offset to 200px. In contrast, scrolling 'by' 200px would change the offset to 300px (initial 100px + 200px).

Always try to relate the name of a method with its purpose. It helps in 99.9% cases!

The method scrollBy() serves this very purpose.

Syntactically, it works exactly like scroll(). But internally it works differently in that the scroll offset is changed by a given value.

Consider the code below:

window.scrollBy(0, 50);

As before, here we call the method on window along with passing it two arguments, which will result in the document being scrolled by that offset.

In the live example below, keep pressing the button to scroll the document by further 50px.

Live Example

For the second case:

window.scroll({
    left: 50,
    top: 300,
    behavior: "smooth"
});

Live Example

Debouncing and throttling

A common pitfall of handling the scroll event is to tie expensive operations with the handler.

For example, suppose a computationally expensive function is represented as expensiveOperation(), the following code portrays the real problem:

window.onscroll = function(e) {
    // perform the expensive operation
    expensiveOperation();
}

The reason it's a problem is directly related with the behaviour of the scroll event - how often it fires.

Essentially, all events that fire at high rates, such as touch, scroll, mouse move, keydown, will compromise the performance of an application when tiring processes are carried on in their handlers.

As you know, the scroll event can potentially occur at each pixel you scroll (on touch devices) on a document. Likewise, if we have an expensive operation being processed on it, even a gentle scroll may stack up hundreds of those expensive operations.

This is clearly inefficient and thus needs rectification. What's the rectification?

We defer the underlying operation.

Defering a scroll event is simply to slow down the rate at which its handler's underlying operation is being performed.

There are two variants of doing this - we can either debounce or throttle the operation.

In debouncing, what happens is that:

We wait until the event doesn't fire for a given amount of time and then perform its underlying operation.

The name comes from electronics where it's used to represent roughly the same idea - only one signal is made to be accepted in a given amount of time.

Debouncing means that only when something stops from happening for a certain amount of time, like the oscillations of a key on a keyboard, do we process it.

The way we debounce an onscroll's underlying operation for 500 millseconds is discussed as follows:

We scroll the document for, let's say 1px, and consequently the scroll event fires once. Now imagine that 500 millseconds haven't elapsed since this happened, that we scroll for another 1px. Since this next scroll event occured before 500ms passed since the previous event's occurence we don't execute our operation.

Now suppose that 600ms have passed since this second scroll event, that we scroll for another 1px (to account for a total of 3px scroll). Since this time, we've has crossed the 500ms mark, we therefore execute our operation.

In simple words, debouncing resets a timer each time a new event is put up before the timer completes.

Live Debouncing Example

This line is very important - we'll use it to construct a debouncing onscroll handler below.

In throttling what happens is that:

We first decide on a time interval and then get the underlying operation to be performed at those intervals.

Once again the name comes from computing where it's used to represent the idea of controlling the rate at which a process is happening.

The way we throttle an onscroll's underlying operation for 500 millseconds is discussed as follows:

A constant check is being run in the background at every 500ms for whether the user is scrolling or not. The check is implemented by giving a global variable some truthy value inside the onscroll handler - usually and sensibly, the value true, and then comparing the value of this variable inside the constant background check.

If it is true, we know that the user has scrolled just recently and consequently perform the underlying operation.

Live Throttling Example

Unlike debouncing, throttling doesn't delay the operation for until the event stops firing - rather it just slows down its rate of execution.
Remember that in both these cases, the actual scroll event isn't being delayed in any way - it is just the underlying operation/function/process that is being delayed!

Whichever variant you choose, both are programmed using the essence of JavaScript Timers - setTimeout() and setInterval().

Let's see whether you can figure out how to code both the variants. Your ultimate JavaScript skills are at a test now!

Debouncing:

Rewrite the onscroll handler below such that it debounces the console log, for the time the scroll event doesn't fire for the next 500 milliseconds, since its last occurence.

You may use setTimeout() for this task.

window.onscroll = function(e) {
    console.log("Hello"); // debounce this
}

At each scroll event we clear any timeout already present in the task queue, by using clearTimeout() and passing it timeout which is meant to hold the ID of the last timeout created. After this we create a new timeout for 500ms, and save this inside timeout.

Following is the solution:

var timeout = null;

window.onscroll = function(e) {
    // clear any previously queued up timeout
    clearTimeout(timeout);

    // then create a fresh, new timeout
    timeout = setTimeout(function(e) {
        console.log("Hello");
    }, 500);
}

Live Debouncing Example

Throttling:

Rewrite the onscroll handler below such that it throttles the console log, for 500ms intervals.

You may use setInterval() for this task.

window.onscroll = function(e) {
    console.log("Hello"); // throttle this
}

In scroll throttling, the interval logic and the scrolling logic are both separate from each other, but obviously linked together.

A 500ms interval is running constantly in the background, checking for whether the user is scrolling right now. If scrolling is being performed, the interval makes the console log and finally sets scrolling to false so that the interval doesn't keep on running!

Following is the solution:

var scrolling = false;
setInterval(function() {
    if (scrolling) {
        console.log("Hello");
        scrolling = false;
    }
}, 500)

window.onscroll = function(e) {
    scrolling = true;
}

Live Throttling Example

And this is it for scroll events...