Introduction

Working with scroll events is quite mainstream 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!

Handling 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 mouse, by pressing the arrow buttons on the scroll bar, by dragging the bar manually, or via the mouse wheel.
  2. Using the keyboard, by pressing the arrow , keys, or the Page Down, Page Up keys etc.
  3. Using touch gestures, by swiping through the screen.
  4. Using JavaScript
  5. 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.

A simple example

Let's 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:

If we want to work with the scroll event, we ought to make sure that the element we'll be working on is scrollable i.e it has a scroll bar.
<div><p>Some content</p></div>
div {
    height: 200px;
    overflow: auto;
    background-color: #ddd
}
p {
    height: 500px;
    /* height exceeds 200px so a scrollbar will be given */
}

Some content

For more info on how to make an element scrollable, consider reading CSS Overflows.

Next, we handle the scroll event on the <div> element:

var div = document.getElementsByTagname("div")[0];

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

This can be done using addEventListener() as well:

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 scroll event is the window object i.e the whole HTML document.

It's common because usually many features of a web application are directly tied with the whole HTML document, such as monitoring ad impressions, lazy loading images, enabling infinite scrolling and so on.

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

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

Live Example

Go on, open the link above and try to scroll the document; you'll 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!

Detecting scroll position

In JavaScript, it's typical to figure out how far has the HTML document been scrolled, vertically or horizontally, in units of pixels.

The language provides us with 4 properties on the window object that enable us to do so.

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. For the older set, we have pageXOffset and pageYOffset.

As the names suggest:

The scrollX and pageXOffset properties, both return the distance by which the document has been scrolled horizontally.

The scrollY and pageYOffset properties, both return the distance by which the document has been scrolled vertically.
All these 4 properties are available on the window object.

Using these properties, we can figure out how far the document has been scrolled in a given axis, and consequently power numerous features that rely on this.

Consider the code below:

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

As before, we are handling the scroll of window, but this time, instead of making fixed "Scrolling" logs, we log the vertical scroll distance of the document.

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 the scroll handler.

You shall assume that the <div> element is 327 pixels away from the bottom of the viewport, and use this value in your scroll handler.

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 #tab-cont 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 to make it scrollable, and then finally handle its scroll event, where we log both its scrollLeft and scrollTop properties:

<div id="area">
    <p>Some content inside this div.</p>
</div>
#area {
    height: 300px;
    width: 80%;
    overflow: auto;
}
p {
    height: 600px;
    width: 120%;
}
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...

Change the scroll position

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 same <div> element above to a 50px vertical scroll offset, we can simply assign 50 to its 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.

First of all, the scrollTop and scrollLeft properties are not available on window. This is because window isn't an element, and as we stated above, these scroll properties are only available on element objects.

Now it maybe tempting to think that since window.onscroll is analogous to document.body.onscroll i.e both point to the same handler, we can use these properties on document.body to scroll the HTML document.

In simpler words, to move the document to a given scroll offset we can use something like document.body.scrollTop = scrollOffset

Strangely, this doesn't work!

This is because the scrollbar that the browser renders for the document belongs to the <html> element, NOT to 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.

To boil it down, scrolling the document using scrollTop or scrollLeft can only be accomplished by using these properties on the documentElement object.

In the code below we change the scroll offset of the document by assigning a value to the scrollTop property of the document.documentElement object:

// scroll the document to 100 pixels vertically
document.documentElement.scrollTop = 100;

Live Example

Construct a function bringInView() that takes in an element node as argument, and operates by scrolling the document to a position such that the element's bottom edge touches the bottom edge of the document's viewport.

An example is shown below:

Content here...

The bottom edge of the element, denoted by the blue box, coincides with the bottom edge of the document's viewport, denoted by the grey bordered box.

function bringInView(ele) {
    // calculate the distance of ele's bottom edge from
    // the bottom edge of the viewport
    var rect = ele.getBoundingClientRect();
    var distance = window.pageYOffset + rect.top + rect.height - window.innerHeight;

    // now, scroll document to the given distance
    document.documentElement.scrollTop = distance;
}

Live Example

Handling scroll on documentElement has no effect.

We just learnt above that the scrollbar on a webpage belongs to the <html> element, and so would naturally think that scrolling the document would fire scroll events on the documentElement object. However this isn't the case!

When we scroll a document, the scroll event does NOT fire on the documentElement object. This means that a scroll handler on documentElement wouldn't execute as the document is scrolled, despite the fact that the scrollbar is actually applied on the documentElement object!

document.documentElement.onscroll = function() {
    // scroll handling code
}

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 just move on.

This is just one of the many quirks JavaScript has to show to its novice learners!

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 the given set of co-ordinates.

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

WindowOrElementObject.scroll(x, y)
  1. x is the distance from the top of the caller object.
  2. y is the distance from the left of the caller object.

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

WindowOrElementObject.scroll(options)
  1. left: same as the x argument
  2. top: same as the y argument
  3. behavior: determines how the scrolling should be done i.e whether it should be performed smoothly using "smooth" or in the blink of an eye using "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 where we demonstrate the scroll() method called with two arguments on the window object:

window.scroll(50, 300);

Executing this piece of code scrolls the document precisely to the given co-ordinates i.e 50px from the left of the document and 300px from the top of the document.

Live Example

Let's now consider an example of calling scroll() with an object argument:

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

The co-ordinates are the same as in the code above. The behavior property, set to "smooth", causes 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, let's suppose that our document is currently scrolled 100px vertically.

From this point on, scrolling vertically to 200px would mean we end up at 200px. In contrast, scrolling vertically by 200px would mean we end up at 300px, since we are now scrolling further by 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 doesn't.

WindowOrElementObject.scrollBy(dx, dy)
  1. dx is the amount by which to further scroll the caller object horizontally. Negative numbers are valid
  2. dy is the amount by which to further scroll the caller object vertically. Negative numbers are valid.

Consider the code below:

window.scrollBy(0, 50);

We call scrollBy() on window along with passing it two arguments, which will result in the document being scrolled by these given values.

Live Example

For the second case:

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

Live Example

The direction of scroll

Sometimes it's irrelevant to know how far has a document been scrolled in a given axis. Rather, what's relevant is just to know the axis of the scroll.

In other words, we're concerned with figuring out the direction in which the document is currently being scrolled. Is the user going upwards, or is he/she going downwards?

This can very easily be accomplished using a bit of clever coding!

The core of this idea lies in a global variable that holds the last scroll co-ordinate of the document (in this case, the y co-ordinate).

Each time the scroll event takes place, we compare the new scroll co-ordinate from this variable, and then decide what is the direction of the scroll.

Let's take an example.

Suppose the global variable lastScrollY holds the last scroll co-ordinate of the document. Initially it's set to 0.

The user scrolls the document, and consequently the scroll event gets dispatched. Inside the event's handler the current scroll co-ordinate is retrieved via windown.pageYOffset. Imagine that it is 1.

The following expression compares both these values:

window.pageYOffset > scrollY

What can we infer about the direction of the scroll from the return value of this expression?

If the expression returns true, this implies that window.pageYOffset is indeed greater than lastScrollY, and consequently the document has been scrolled downwards.

On the other hand, if the expression returns false this implies that window.pageYOffset is lesser than lastScrollY, and therefore the document has been scrolled upwards.

Now that we know about the expression to use to check the scroll's direction, let's finally incorporate it into a fully functional piece of code:

var lastScrollY = window.pageYOffset;

window.onscroll = function() {
    if (window.pageYOffset > lastScrollY) {
        console.log("Scrolling downwards");
    }
    else {
        console.log("Scrolling upwards");
    }

    // update to the current value
    lastScrollY = window.pageYOffset;
}

In line 1, a global variable lastScrollY is created and initialised to whatever location the document is scrolled currently.

Next we define the onscroll handler on window and put a conditional statement to evaluate the value of the expression and decide based on it the direction of the scroll.

Notice line 12 here - it updates lastScrollY to the latest scroll position so that on subsequent scroll events, the correct scroll direction is detected.

Live Example

Performance issues

A common mistake done while handling the scroll event is to tie expensive operations with the handler, which results in some serious performance issues on even high-end devices.

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, mousemove, 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.

Debouncing

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.

Rewrite the onscroll handler below such that it debounces the console.log() statement 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

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!

Rewrite the onscroll handler below such that it throttles the console.log() statement 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...