meta-capi-dedup

Why Use a Custom JavaScript Variable for Scroll Depth Tracking in GTM?

Scroll depth tracking tells you how far down your pages visitors actually read—a critical engagement metric for content sites, landing pages, and long-form blog posts. GA4 includes a built-in scroll tracking feature that fires a scroll event when a user reaches 90% of a page’s length. But 90% is a single threshold. For most content analysis purposes you want multiple thresholds—25%, 50%, 75%, and 90%—so you can see not just who finished reading but where the majority of readers drop off.

GTM’s built-in Scroll Depth trigger handles percentage-based thresholds automatically, but it lacks flexibility for more nuanced use cases: tracking scroll depth relative to a specific content element (not the whole page), firing only after a minimum time on page, or combining scroll depth with other interaction signals. A custom JavaScript variable in GTM gives you complete control over how scroll depth is calculated and when it is reported, enabling engagement tracking tailored exactly to your content’s structure.

Option 1: Use GTM’s Built-In Scroll Depth Trigger

Before building a custom solution, check whether GTM’s built-in trigger meets your needs. In GTM, create a new Trigger of type “Scroll Depth.” Choose “Vertical Scroll Depths” and enter percentages: 25, 50, 75, 90. Set the trigger to fire on “All Pages” or filter to specific page paths. Create a GA4 Event tag that fires on this trigger, with event name scroll_depth and an event parameter scroll_percentage set to the built-in variable “Scroll Depth Threshold.”

This setup covers the most common scroll tracking requirement in under 10 minutes. Use it as your default approach. Build a custom JavaScript variable only when you need behavior the built-in trigger cannot provide—such as element-relative scroll tracking or debounced scroll reporting.

Option 2: Custom JavaScript Variable for Advanced Scroll Tracking

For more advanced use cases, create a Custom JavaScript variable in GTM that calculates scroll depth on demand. This variable runs when called and returns the current scroll percentage. In GTM, go to Variables → User-Defined Variables → New → Custom JavaScript. Paste the following function:

function() {
  var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
  var docHeight = Math.max(
    document.body.scrollHeight,
    document.documentElement.scrollHeight,
    document.body.offsetHeight,
    document.documentElement.offsetHeight,
    document.body.clientHeight,
    document.documentElement.clientHeight
  ) - window.innerHeight;
  
  if (docHeight <= 0) return 0;
  
  var scrollPercent = Math.round((scrollTop / docHeight) * 100);
  return Math.min(scrollPercent, 100);
}

Name this variable "JS - Scroll Depth Percent." It returns an integer from 0 to 100 representing how far down the page the user has scrolled. The Math.max pattern handles cross-browser differences in how document height is calculated. The Math.min(..., 100) cap prevents values above 100% in edge cases where dynamic content loads after the initial height is measured.

Building the Scroll Milestone Trigger

The custom JavaScript variable alone does nothing—you need a trigger that fires at scroll milestones and calls this variable. Create a GTM trigger of type "Custom Event" named "scroll_milestone." Then add a Custom HTML tag that fires on a Window Loaded trigger and injects a scroll listener into the page:

<script>
(function() {
  var milestones = [25, 50, 75, 90];
  var fired = {};
  
  function checkScroll() {
    var scrollTop = window.pageYOffset || document.documentElement.scrollTop || 0;
    var docHeight = Math.max(
      document.body.scrollHeight, document.documentElement.scrollHeight,
      document.body.offsetHeight, document.documentElement.offsetHeight
    ) - window.innerHeight;
    
    if (docHeight <= 0) return;
    var pct = Math.round((scrollTop / docHeight) * 100);
    
    milestones.forEach(function(m) {
      if (pct >= m && !fired[m]) {
        fired[m] = true;
        window.dataLayer = window.dataLayer || [];
        window.dataLayer.push({
          event: 'scroll_milestone',
          scroll_depth_percent: m,
          page_path: window.location.pathname
        });
      }
    });
  }
  
  var ticking = false;
  window.addEventListener('scroll', function() {
    if (!ticking) {
      window.requestAnimationFrame(function() {
        checkScroll();
        ticking = false;
      });
      ticking = true;
    }
  }, { passive: true });
})();
</script>

This script uses requestAnimationFrame for debouncing—it will not fire on every scroll pixel, only once per animation frame, keeping CPU usage minimal. The fired object ensures each milestone fires only once per page load. The { passive: true } event listener option tells the browser this listener will not call preventDefault(), improving scroll performance on mobile.

Creating the GA4 Tag for Scroll Milestones

Create a new GA4 Event tag in GTM. Set the event name to scroll_milestone. Add an event parameter: key = scroll_depth_percent, value = a Data Layer Variable named scroll_depth_percent (which reads from the dataLayer push in the Custom HTML script above). Add a second parameter: key = page_path, value = the built-in GTM variable "Page Path."

Set the trigger to the Custom Event trigger named "scroll_milestone." Now every time a user scrolls past 25%, 50%, 75%, or 90% of a page, a scroll_milestone event fires in GA4 with the exact threshold reached and the page path. In GA4 Explore, you can then build a table showing average scroll depth by page, or the percentage of users who reach 75%+ scroll depth sorted by page—immediately revealing which pages hold attention and which lose readers early.

Element-Relative Scroll Tracking

For long pages with content above the fold (hero sections, navigation, sidebars), page-relative scroll depth can be misleading. A user who scrolls 50% of the page may have only read 10% of the actual article content. Element-relative scroll tracking measures scroll depth relative to a specific DOM element—like the article body—rather than the full page. Modify the Custom HTML script to calculate scroll position relative to your content container's top and bottom offsets using getBoundingClientRect() on the target element. This gives you true content engagement depth, independent of page layout.

Analyzing Scroll Data in GA4

Once scroll milestone events are flowing into GA4, build a Free Form Exploration with Event Name = "scroll_milestone" filtered, scroll_depth_percent as a dimension in rows, and Event Count as the metric. This table shows how many times each threshold was reached across all pages. Divide each threshold count by the 25% count to get a drop-off rate: if 10,000 users hit 25% but only 4,000 hit 75%, you have a 60% drop-off between those milestones—a strong signal that content quality or length is a problem. Add Page Path as a secondary dimension to see which specific pages have the best and worst scroll retention.

Conclusion

Custom JavaScript scroll depth tracking in GTM—whether using the built-in trigger for simple needs or a custom script for advanced element-relative or debounced tracking—transforms your understanding of content engagement. Page views tell you who visited; scroll depth tells you who actually read. By routing scroll milestone events into GA4 and analyzing them in Explore reports, you gain concrete data to prioritize content improvements, optimize page length and structure, and prove content engagement value to stakeholders who need more than a pageview count.

Leave a Comment