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!
What is scrollable?
Before we can listen to scroll events on different elements, it's vital to understand when exactly can we do so.
Let's understand what does it mean to say that an element is scrollable:
The most common scrollable element on a webpage is the whole document itself.
On almost all websites out there, we have a scroll bar on the right hand side of the document implying that the document is scrollable.
You can see the scroll bar on this website as well, if you're on a desktop device. On mobile and tablet devices the scroll bar is usually hidden by default, unless we tweak the CSS to show it.
Other elements can also be made scrollable by using CSS.
When the overflow
CSS style property of any HTML element is set to scroll
or auto
and the content within that element overflows its dimensions, the browser automatically adds a scroll bar to enable viewing that overflowing content.
overflow
style property at CSS Overflows.The scroll event in JavaScript is only meaningful as long as there is a scroll bar in the respective element. If the element is not scrollable, then there is no point of listening to the scroll event.
Handling the scroll
event
Now that we know when is the scroll event meaningful to be handled on any given element, it's time to dive into its details.
Each time an element is scrolled, a scroll
event is fired on it.
How to scroll an element?
Well, there are quite a few ways to do so. They are listed as follows:
- By clicking the arrow buttons on the scroll bar.
- By dragging the scroll bar manually using the mouse pointer.
- Using the mouse wheel.
- Using the keyboard, by pressing the arrow
↑
,↓
keys, or thePage Down
,Page Up
keys etc. - Using touch gestures, by swiping through the screen.
- Using JavaScript
- By visiting an ID link
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"
.
Scrolling increments
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 scroll position.
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()
.
Once scrolling is done, subsequent actions can be made using the onscroll
handler:
targetObject.onscroll = function() {
// handling code
}
..or even by listening to the scroll
event using addEventListener()
:
targetObject.addEventListener("scroll", function() {
// handling code
});
addEventListener()
method, refer to JavaScript Event Handlers and JavaScript addEventListener()
method.Let's consider a quick example of handling the scroll
event.
A simple example
Below we create a <div>
element and assign it some CSS styles in order to make it scrollable:
<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
Next, we handle the scroll
event on the <div>
element using the onscroll
property:
var div = document.getElementsByTagname("div")[0];
div.onscroll = function(e) {
console.log("Scrolling");
}
Inside the handler, we simply log the string 'Scrolling'
.
With all this is place, we get the following result:
<div>
.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");
}
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:
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.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.
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%
}
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");
}
}
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%;
}
var area = document.getElementById("area");
area.onscroll = function() {
console.log(this.scrollLeft, this.scrollTop);
}
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 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>
var area = document.getElementById("area");
area.scrollTop = 50;
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.
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;
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:
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;
}
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 behavior of browsers, and just move on.
This is just one of the many quirks JavaScript has to show to its novice learners!
DOM 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)
x
is the distance from the top of the caller object.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)
left
: same as thex
argumenttop
: same as they
argumentbehavior
: 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"
.
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.
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.
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
.
The method scrollBy()
serves this very purpose.
Syntactically, it works exactly like scroll()
, but internally it doesn't.
WindowOrElementObject.scrollBy(dx, dy)
dx
is the amount by which to further scroll the caller object horizontally. Negative numbers are validdy
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.
For the second case:
window.scroll({
left: 50,
top: 300,
behavior: "smooth"
});
Detecting scroll direction
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).
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 its scroll
event gets dispatched. Inside the event's handler, the current scroll co-ordinate is retrieved via windown.pageYOffset
.
The following expression compares both these values:
window.pageYOffset > lastScrollY
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.
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 behavior 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:
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.
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.
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);
}
Throttling
In throttling what happens is that:
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.
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;
}
And this is it for scroll events...