Scroll Tracking for Single-Page Applications (GTM)

API Connector Add-On for Google Sheets

Check out my API Connector Add-on to easily connect and pull data from thousands of platforms (e.g. Shopify, Harvest, Mailchimp, ActiveCampaign, Google Ads, YouTube, etc.) directly into Google Sheets.

Google Tag Manager (GTM) conveniently provides a scroll depth trigger to determine how far someone has scrolled down the page. However, this scroll depth trigger doesn't work on websites using single page app (SPA) technology, since there is no full page load to reset the thresholds. This means that once you scroll down to the bottom and hit your maximum scroll threshold, scroll tracking on subsequent pages won't be tracked.

This article will show how to set up scroll tracking for single page apps using GTM (and only GTM). Since GTM's built-in scroll tracking won't work here, we'll modify a different scroll tracking plugin provided by Lunametrics (now known as Bounteous). They've done all the hard work by providing the script, we just need to make a couple small changes.

While this method should work for most cases, there will certainly be sites where it does not. As always, test before publishing.


  1. Get the Lunametrics scroll tracking plugin
  2. Import the container into GTM
  3. Choose how to import the file
  4. Complete the import process
  5. Select the scroll tracking tag
  6. Edit the scroll tracking trigger
  7. Upate the scroll tracking tag
  8. Add in your GA tracking ID
  9. Modify, preview, and publish

1. Get the Lunametrics scroll tracking plugin

Download the Lunametrics scroll tracking container file here. Right-click the link and select "Save link as" to save the file to your own computer.

2. Import the container into GTM

Log into GTM and click Admin > Import Container

3. Choose how to import the file

You'll be prompted to choose the file to import, which should be called luna-scroll-tracking.json. Choose to save it into a new or existing workspace (I recommend choosing New, since it's more convenient for testing, and allows you to easily scrap the whole thing if you run into any problems).

You'll also be prompted to choose whether these new tags, triggers, and variables should overwrite or merge into your existing container. Choose "Merge" if you want to ensure that no existing tags are disrupted.

4. Complete the import process

Once you've chosen your settings, click Confirm to import the Lunametrics scroll tracking container into your own GTM container.

You should now see a set of new tags, triggers, and variables related to scroll tracking. This means we can now add in our modifications to make it work for a single page app setup.

5. Select the scroll tracking plugin tag

Head over to the Tags section of GTM and find the CU - Scroll Tracking - LunaMetrics Plugin. By default, they've configured it to fire on All Pages. However, we want it to wait for the whole page to load, because single page apps often have an issue in which the tag fires too early and immediately sends events for all scroll tracking thresholds. Click into the tag so we can modify the trigger.

6. Edit the scroll tracking trigger

Edit the trigger by clicking on the icon in the top right of the trigger section. Remove the existing 'All Pages' trigger.

Replace that Page View trigger with a Window Loaded trigger and click Save.

7. Update the Custom HTML tag

This step is the core modification to make the scroll tracking plugin work on our single page app. This section has been updated based on contributions from commenter Kriya, who says:

Replace the method onDocHeightChange @line 505 in the LunaMetrics plugin to

 * Helper that watches for changes to location.href
function onDocPathChange(handler) {
  var href = location.href;

  return setInterval(function() {
    if (location.href !== href) {
      href = location.href
  }, 500);

then, on line 83, update its reference from timer: onDocHeightChange(boundUpdate), to timer: onDocPathChange(boundUpdate),

It basically watches for changes to location.href and resets the internal state of the plugin (when it calls handler callback parameter), instead of doing that when the height of the document changes. One thing to keep in mind is that location.href includes the domain, path, along with any query params and hash at the end. So changes to any of that would cause the plugin state to reset, and start re-triggering the scrollTracking event. It may be mentioned that some people may want to modify that method to instead rely on location.pathname (which would ignore changes to query params or hash -- hash generally changes when a link is clicked that anchors to an element on the same page, in which case it may be better to not reset the state as the page hasn't actually changed). I've tested this change and it's been working well for me.

Thank you, Kriya! Now, this function should run to clear and reset the scroll thresholds, allowing you to track subsequent pages in your app as if your site were firing traditional page loads.

8. Add in your own GA tracking ID

Find the tag called GA - Event - Scroll Tracking and update it with your own GA tracking ID.

9. Modify, Preview, and Publish

Now that you have everything set up, enter preview mode and click around your site to verify that all is working as expected. You may also wish to adjust the thresholds for your trigger, which you can do within the original CU - Scroll Tracking - LunaMetrics Plugin tag. By default, they've set it to fire at 10% and 90%, as well as every 25% (i.e. at 25%, 50%, 75%, and 100%). In this example adjustment, I've set it to fire only at the 25% intervals, to reduce the total quantity of scroll tracking events.

You may also wish to modify your event to fire only on certain URLs. Once you've made your modifications and verified that all works in Preview mode, click Publish to send the new scroll tracking tags to your site.

Please leave a comment if this works (or doesn't work) for you!

10 thoughts on “Scroll Tracking for Single-Page Applications (GTM)”

  1. Thank you for this article! I have been working on this work a week and this solves my problem! Thank you for putting this together!

  2. Great stuff! I loved working with Luna while I was at Caterpillar. Nice mod. One interesting thing I noticed (or maybe didn't adjust right) is that the scroll tracking doesn't like to fire again when you return to the same page in a session. For example user moves from A (first time) to B to C to A (second time)

    It seems that the tag fires on A (first time), B, & C as expected but fails to fire when the user returns to page A for the 2nd time.

    • Thanks for letting me know! On my test site, it seems to work even on second page views, but I will update the post to note that it might not always work as expected.

  3. Question on Horizontal scrolling; I have a DIV container on my SPA landing page. It scrolls deep horizontally but the rest of the page doesn't. Will this work just for that DIV container?

    • Sorry, good question but I don't know the answer as I didn't create the script myself. I would just test it out and see how it goes.

  4. Looks like step 7, the one said to be the core step to make the plugin work on single-page apps, is not doing anything. The function is never called, and even if it were called (as an IIFE), the result would be an error as `this` would refer to Window object that does not have any `_artifacts` property defined.

    Those three statements are a copy of ScrollTracker class' `destroy` method from the plugin (which is never actually used in the plugin itself anyway).

    To test it yourself, try removing that Custom HTML tag from step 7 and navigate around your site/app. The events will still be triggered for any page that has a different height than the previous one as the plugin has a method, `onDocHeightChange`, that checks for changes in the height of the document every half second, resetting internal state if it does.

    However, that internal logic will not work for any site or app that has a fixed height for pages, so I've updated it to work on history change.

    • Hi Kriya, thank you for the comment! This is very interesting to read. Though I'm not totally sure what to make of it, because it seems to work fine as is on the SPA sites I've tested it on, and when I remove the custom HTML tag from step 7, it no longer works. Do you have any suggestions for how to make this a more reliable scroll tracking script for SPA pages?

      • Well, try putting a simple `console.log` statement (or alert() for something more obvious) in that anonymous function and you'll notice that it isn't really called.

        I basically updated `onDocHeightChange` to be `onLocationChange` which would check for changes to `location.href` every half a second and reset internal state, same as `onDocHeightChange` did.

Comments are closed.