Scroll Events
Probably the highest occurring event in a browser window is the scroll
event. When the user scrolls a page, or an element, the scroll
event fires. The scroll event lets us know which part of the page the user is viewing, so we could use this information to change some elements' CSS, or switch off some calculation heavy animation that is not in view anymore.
Lets begin with a simple example. We will create a page with a set height. Then we will listen for the scroll event on window
. Whenever the user scrolls, we will update a percentage number on the screen fixed to the top left corner that shows how much of the page has left to scroll.
<div id="progress">0%</div>
body {
margin: 0;
height: 3000px;
}
#progress {
position: fixed;
top: 20px;
left: 20px;
}
var progress = document.body.querySelector('#progress');
window.addEventListener('scroll', function (event) {
var scrollableHeight = document.body.scrollHeight - window.innerHeight;
var percentage = Math.round((window.pageYOffset / scrollableHeight) * 100);
progress.innerHTML = percentage + "%";
});
A bit of sidetrack here. In order for us to calculate the percentage of the page a user has viewed, we need to know the total scrollable height the user can scroll through. There are a few properties we need to look at to get the number we need.
Element.scrollHeight
: This is the full height of an element's content plus padding, including content that is not visible on the screen. It excludes horizontal scrollbar height, border and margin. If we have a div
element that has a lot of content, such as paragraphs, we could limit the div
's viewable height to say 500px. The scrollHeight
of the div
in this case would be larger than its CSS defined height.
Element.clientHeight
: Extending the example described above, the clientHeight
of the div
would be the visible area displayed on the screen. clientHeight
includes padding but does not include horizontal scrollbar height.
window.innerHeight
: This is the height of the window's viewport including the height of the horizontal scrollbar. In other words, this is the height of the screen that is displaying the page.
Because a part of the body is always going to be displayed on the screen, the height that is left to be scrolled would be the full content height of the page, minus the height of the window; document.body.scrollHeight - window.innerHeight
.
To get how much the user has scrolled so far, we can query window.pageYOffset
, it is measured in px
. So the percentage the user has scrolled is just (window.pageYOffset / document.body.scrollHeight - window.innerHeight) * 100
.
Try it on Repl
Detecting End of Scroll on Element
The above example works on the body
element and when the scroll event happens on the window
. Sometimes, we might have a smaller element on the page that we require the user to read but is contained by a max height value. Such could be the Terms and Conditions during user account creation. When dealing with an element that has a defined height, and its content overflowed, we need to query different properties. Imagine the following example.
<form>
<div id="terms">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. In ut ipsum id lectus tempus viverra. ...
</p>
</div>
<input type="checkbox" id="agree" name="agree" disabled/>
<label for="agree">I Agree</label>
</form>
#terms {
height: 200px;
border: 1px solid black;
border-radius: 2px;
overflow: scroll;
}
The p
element's actual content height is larger than the 200px
set on #terms
. We set #terms
to overflow: scroll;
so the user needs to scroll the content to view all the text. We want to know when the user has scrolled all the way to the end. Then we can enable the check box input at the bottom.
This is very similar to the first case, we have to listen for the scroll event on #terms
. The calculation to find out if the user has reached the end is theoretically the same, the only thing changed is the properties we need to query.
To get the full content height of the scrollable element #terms
, we can query the Element.scrollHeight
property. To figure out how much of this height is actually scrollable, we need to determine how much of the content is hidden away. We can check Element.clientHeight
to find out the height of the visible area of an element. Naturally, the height of the content that is hidden away is just scrollHeight - clientHeight
Now we just need to know how much the user has scrolled so far. Each scrollable element has a property called Element.scrollTop
which returns in px
how much the content is scrolled vertically.
We will know the user has scrolled to the bottom of the #terms
element when scrollHeight - clientHeight
is equal to scrollTop
.
var terms = document.body.querySelector('#terms');
var agree = document.body.querySelector('#agree');
var enableCheckbox = function (event) {
var scrollableHeight = terms.scrollHeight - terms.clientHeight;
if (scrollableHeight === terms.scrollTop) {
agree.disabled = false;
terms.removeEventListener('scroll', enableCheckbox);
}
}
terms.addEventListener('scroll', enableCheckbox);
It's also good to clean up after ourselves, that's why we removed the handler once our checkbox is enabled.
Info:
Element.scrollTop
is also a settable property, you can use it to set the scroll distance of an element from its top.
Try it on Repl
Fixed Top NavBar With Changing Background Color
Lets go through another example that is commonly applied in the real world. To make browsing more convenient for users, many designers will set the navigation bar fixed at top. The designer would often apply a different set of styles to the nav bar when it's in its initial position compared to when the page has been scrolled. We can easily do this by listening to the scroll event on window
then toggle a class on the nav bar that changes the styles.
<body>
<nav id="top-nav">...</nav>
<div id="hero">...</div>
<div>Content</div>
</body>
#top-nav {
padding: 20px;
position: fixed;
top: 0;
width: 100%;
}
#top-nav a {
color: #FFF;
}
#top-nav.inverse {
background-color: rgba(255, 255, 255, 0.5);
}
#top-nav.inverse a {
color: #1976D2;
}
We would only apply the .inverse
class to nav only after we have scrolled pass the #hero
element. So in our event handler, we need to know the height of the #hero
element.
var nav = document.body.querySelector('#top-nav');
var hero = document.body.querySelector('#hero');
window.addEventListener('scroll', function (event) {
if (window.pageYOffset > hero.scrollHeight) {
nav.classList.add('inverse');
} else {
nav.classList.remove('inverse');
}
});
Try it on Repl