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:

  1. 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.
  2. 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).
  3. 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.

  1. Scroll Depth: >= 25%
  2. Scroll Depth: >= 50%
  3. 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.

Christoph Schiessl

Hi, I'm Christoph Schiessl.

I help you build robust and fast Web Applications.


I'm available for hire as a freelance web developer, so you can take advantage of my more than a decade of experience working on many projects across several industries. Most of my clients are building web-based SaaS applications in a B2B context and depend on my expertise in various capacities.

More often than not, my involvement includes hands-on development work using technologies like Python, JavaScript, and PostgreSQL. Furthermore, if you already have an established team, I can support you as a technical product manager with a passion for simplifying complex processes. Lastly, I'm an avid writer and educator who takes pride in breaking technical concepts down into the simplest possible terms.

Continue Reading?

Here are a few more Articles for you ...


Detecting Exit Intent with Vanilla JavaScript

Use event handlers to detect exit intent so you can react to grab your website visitors' attention before leaving and perhaps convert them into repeat visitors.

By Christoph Schiessl on JavaScript

Extracting all URLs of your sitemap.xml with JavaScript

Learn JavaScript techniques needed to parse your sitemap.xml in order to obtain a list of all pages making up your website.

By Christoph Schiessl on JavaScript

How to Validate Word Counts for SEO

Learn how to validate the word count of your pages for SEO purposes. It uses JavaScript to parse your sitemap.xml and count the words on all mentioned pages.

By Christoph Schiessl on JavaScript

Web App Reverse Checklist

Ready to Build Your Next Web App?

Get my Web App Reverse Checklist first ...


Software Engineering is often driven by fashion, but swimming with the current is rarely the best choice. In addition to knowing what to do, it's equally important to know what not to do. And this is precisely what my free Web App Reverse Checklist will help you with.

Subscribe below to get your free copy of my Reverse Checklist delivered to your inbox. Afterward, you can expect one weekly email on building resilient Web Applications using Python, JavaScript, and PostgreSQL.

By the way, it goes without saying that I'm not sharing your email address with anyone, and you're free to unsubscribe at any time. No spam. No commitments. No questions asked.