Tracking Scroll Depth to Measure Visitor Engagement
by Christoph Schiessl on JavaScript
One key metric you can track to measure and better understand how visitors engage with your website is scroll depth. The idea is simple: You use JavaScript to determine which parts of the current page a visitor has already seen and submit corresponding events to your analytics platform.
There are multiple ways in which the visible parts of a page can change over time, and we have to handle all of them:
- Initial page load. As soon as the page is fully loaded, the visitor sees the first part of it. If the content's height is less than the viewport's height, then the visitor immediately sees the whole page. Otherwise, the visitor initially only sees the first
X%
of the page. - Resizing the viewport. When the visitor changes the viewport's size (e.g., by resizing the browser window), it can also affect the visible parts of the page. This is especially true if the resizing causes a layout change, which is very common if different CSS rules become active (i.e., if a different breakpoint matches).
- Scrolling. Lastly, if the visitor actively scrolls in any direction, then the visible part of the page must change accordingly.
This was a high-level overview of the events we need to handle to always keep track of the parts of the page the visitor has already seen.
Event Listeners
In JavaScript terms, we need three event listeners to implement this ...
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Scroll Depth Demo</title>
<script>
function trackResizeAndScroll() {
console.log("trackResizeAndScroll() was called.");
}
window.addEventListener('load', trackResizeAndScroll);
window.addEventListener('resize', trackResizeAndScroll);
document.addEventListener('scroll', trackResizeAndScroll);
</script>
</head>
<body>
<h1>Scroll Depth Demo</h1>
</body>
</html>
Firstly, for the page's initial load, we can use the load
event of the window
object. This event is fired only once, and we will use it to determine the part of the page that's initially visible. It's important to understand that all of the page's external resources, such as stylesheets, are already loaded when the browser fires this event. Consequently, the height of the page's content shouldn't change after the event was fired (assuming the page has no interactivity that causes the content height to change).
Secondly, to catch changes in the viewport's size, we can use the window
object's resize
event. As the name suggests, this event is fired when the viewport size changes, including zooming in and out. In any case, as mentioned before, a different viewport size can potentially impact the page's content height, meaning that we have to re-calculate the scroll depth.
Thirdly, to be notified when the visitor actually scrolls in any direction, we have to listen to the scroll
event of the document
object. This event is fired when the scroll position changes, which obviously also changes the current scroll depth.
Calculating the Scroll Depth
Now that we have the event listeners set up, it's time to implement the trackResizeAndScroll()
function. Ultimately, we want a percentage value (i.e., a float between 0.0
and 1.0
) telling us which fraction of the page the visitor has already seen. Before we can do that, though, we need to discuss a few more JavaScript APIs to get the required inputs from the window
and document
objects.
function trackResizeAndScroll() {
// TODO: To make the calculation, we need the content height,
// the scroll position and the viewport height.
const percentage = 0.0;
}
Alright, to get the content height, we can use the read-only scrollHeight
property of the page's <body>
element. To get a reference to the <body>
element, we can use the body
property of the global document
object, which leaves us with the following:
function trackResizeAndScroll() {
// TODO: To finish the calculation, we need the scroll position and the viewport height.
const percentage = 0.0 / document.body.scrollHeight;
}
Next, to get the scroll position, we can use the read-only scrollY
property of the window
object. This value represents the distance that the visitor has already scrolled down. In other words, it is the fraction of the content height that is no longer visible because the visitor has scrolled down by that much. Initially, after loading the page, this value should always be equal to 0
.
function trackResizeAndScroll() {
// TODO: To finish the calculation, we still need the viewport height.
const percentage = window.scrollY / document.body.scrollHeight;
}
As it stands now, our calculation will never give a result of 100%
because we are not taking the viewport itself into account. Even worse, if the page doesn't have a scrollbar, we are always calculating 0%
.
To compensate for this, we have to add the viewport height to the scroll position, which makes total sense if you think about it. The visitor has seen everything currently in the viewport, and everything that is, due to scrolling down, is no longer in the viewport. That said, to get the viewport's height, we can use the read-only innerHeight
property of the window
object.
function trackResizeAndScroll() {
const percentage = (window.scrollY + window.innerHeight) / document.body.scrollHeight;
}
Note that this calculation works for vertical scrolling, but you can easily adapt it to horizontal scrolling. All you need to do is to replace the properties to go into the calculation. For instance, you would replace scrollY
with scrollX
and so on.
Tracking Events with Plausible Analytics
I can't overstate my distaste for cookie consent banners. Therefore, I'm a big fan of Plausible because their analytics platform is fully compliant with the GDPR and doesn't require any opt-ins from visitors.
By default, Plausible provides only minimal tracking of page views, but it also supports custom events, which we'll use. When a certain scroll depth is reached, we use Plausible's JavaScript API to track a custom event for it.
To make this work, we must map the continuous scroll depth changes to discrete events. In principle, we could track as many custom events as we like, but for now, we are going with three named events that should be pretty much self-explanatory.
Scroll Depth: >= 25%
Scroll Depth: >= 50%
Scroll Depth: >= 75%
To access Plausible's JavaScript API, we have to include their JavaScript snippet and then define the global plausible()
function ourselves because, initially, it will not be available since their <script>
tag is marked as deferred.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Scroll Depth Demo</title>
<!-- Plausible Analytics JavaScript Snippet -->
<script defer data-domain="example.com" src="https://plausible.io/js/script.tagged-events.js"></script>
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
<script>
function trackResizeAndScroll() {
const percentage = (window.scrollY + window.innerHeight) / document.body.scrollHeight;
if (percentage >= 0.25) { plausible("Scroll Depth: >= 25%"); }
if (percentage >= 0.50) { plausible("Scroll Depth: >= 50%"); }
if (percentage >= 0.75) { plausible("Scroll Depth: >= 75%"); }
}
window.addEventListener('load', trackResizeAndScroll);
window.addEventListener('resize', trackResizeAndScroll);
document.addEventListener('scroll', trackResizeAndScroll);
</script>
</head>
<body>
<h1>Scroll Depth Demo</h1>
</body>
</html>
This implementation has one big drawback that we still have to address. Namely, it sends Plausible events whenever one of the events fires, which is clearly not what we intended. Instead, we should send each event only once, and in particular, we shouldn't send events again if a visitor scrolls back up after scrolling down.
Fortunately, this is easy to solve with a few boolean variables that live outside the trackResizeAndScroll()
function. Basically, we set these variables to true
before we send the particular event so that we know later on not to send the same event again.
Alright, here is the complete implementation in all of its glory:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Scroll Depth Demo</title>
<!-- Plausible JavaScript Snippet -->
<script defer data-domain="example.com" src="https://plausible.io/js/script.tagged-events.js"></script>
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
<script>
let scrolledTo25 = false, scrolledTo50 = false, scrolledTo75 = false;
function trackResizeAndScroll() {
const percentage = (window.scrollY + window.innerHeight) / document.body.scrollHeight;
if (percentage >= 0.25 && !scrolledTo25) { scrolledTo25 = true; plausible("Scroll Depth: >= 25%"); }
if (percentage >= 0.50 && !scrolledTo50) { scrolledTo50 = true; plausible("Scroll Depth: >= 50%"); }
if (percentage >= 0.75 && !scrolledTo75) { scrolledTo75 = true; plausible("Scroll Depth: >= 75%"); }
}
window.addEventListener('load', trackResizeAndScroll);
window.addEventListener('resize', trackResizeAndScroll);
document.addEventListener('scroll', trackResizeAndScroll);
</script>
</head>
<body>
<h1>Scroll Depth Demo</h1>
</body>
</html>
Conclusion
There are many ideas around user tracking, and scroll depth is just one metric that I find particularly useful. Anyway, thank you very much for reading! Please don't forget to subscribe to my mailing list if you enjoyed this article and learned something new.