Track and measure scrolling

Update: there is a new lighter-weight version of the scroll tracking code here. The older version here has the advantage of automatically adapting to changes in page height if the browser window is resized or new content loads. However, I have had one report of this causing jittery scrolling, the lighter weight version is designed to avert any issues with scrolling

Why measure scrolling behaviour?

Scrolling behaviour can be a great indicator of content engagement. In my previous post I discussed some of the reasons we need better measures of content engagement than GA gives us out of the box.

My next post will cover reporting and analysing the data collected; broadly, the script can be used to

  • track content engagement levels by page, site area, audience source or campaign
  • identify under performing pages, where people are not reading the content
  • find content and design issues in a large site – see where some pages may be longer than they should be
  • see the impact of responsive design changes in terms of scrolling behaviour and identify situations where the responsive design in breaking

How to measure scrolling

The script below reports an event to Google Analytics when the page is scrolled 25, 50, 75 and 100% of the way to the footer. It also reports the number of pixels scrolled to reach that point as the event value.

It does not require jQuery, or any other libraries

The script will work with Google Tag Manager (GTM), as well as a stand-alone Google Universal analytics implementation. Adapting it to work with the classic asynchronous Google Analytics would be simple

In the default GTM configuration, the script fires events into the Google Tag Manager dataLayer, which can then be picked up by other tags and scripts.

The script can be adapted for a wide range of tracking scenarios.

It is configured to report events only once, rather than each time the page is scrolled past a point – again, you can modify if you require something different.

Implementing the script

The script below can be used in two ways: with or without Google Tag Manager (GTM).

Implementing without GTM

simply add the script to each page, ideally in thesection after the universal analytics tag. Remember to change the value of the useGTM variable to false.

Implementing with GTM

This is the recommended approach, as it gives you all the benefits of Google Tag Manager and is far easier to customise and control the way the events come through.
For the purposes of these instructions, I’m assuming that you already have universal analytics working in a GTM container, and have some knowledge of GTM. If you need help with this Google provides a good guide to why and how to use Google Tag Manager

  1. Add the script as a custom HTML tag, set to fire on all pages.
  2. Add Data Layer Variables for eventCategory, eventAction, eventLabel and eventValue
  3. Add a trigger, for a custom event with event name=scroll
  4. Add a universal analytics event tag, to fire on the trigger from (3) and get its Category, Action, Label and Value from the variables in (2)
  5. Put the container into preview mode and test

The script

Three configuration variables are brought to the top for ease of tweaking:

  • footerElementID=”footer” – if you have a footer in a div or similar, set this to its ID. If you do not, set this to anything, but make sure it does not match any actual element
  • nonInteraction=”true” – by default the events are non-interactive. This impacts whether the event is seen as an interaction for the purposes of calculating bounce rate (see this Google reference). If you are using GTM you can set whether the events are interactive in the GTM setup itself (step (4)), or you can pick up this as a data layer variable. If you are reporting directly to GA, then this variable will be in control.
  • useGTM=true – if true, the code will send events to the GTM dataLayer. If false, the code will use ga(‘send’, {…

Minified version

<script>// <![CDATA[
function getScrollHandler(){
var footerElementID="footer", nonInteraction="true", useGTM=true;
var e,t,a=0,r=document.body,i=document.documentElement,c=document.getElementById(footerElementID);return t=useGTM?function(e,t){dataLayer=window.dataLayer||[],dataLayer.push({event:"scroll",eventCategory:"scroll",eventAction:"scroll",eventLabel:e,eventValue:t,nonInteraction:nonInteraction})}:function(e,t){ga("send",{hitType:"event",eventCategory:"scroll",eventAction:"scroll",eventLabel:e,eventValue:t,nonInteraction:nonInteraction})},function(){var n=Math.max(r.scrollHeight,r.offsetHeight,i.clientHeight,i.scrollHeight,i.offsetHeight),o=Math.max(i.clientHeight,window.innerHeight||0),l=Math.floor(Math.abs(r.getBoundingClientRect().top)),d=d=null===c?n-o:n-o-c.offsetHeight;e=Math.min(100,25*Math.floor(l/d*4)),e>a&&(a=e,t(e+"%",l))}}
// ]]></script>

Full version

<script>// <![CDATA[
if (window.addEventListener) {
    window.addEventListener("scroll", getScrollHandler());
} else if (window.attachEvent) {
    window.attachEvent("onscroll", getScrollHandler());

function getScrollHandler() {
    var footerElementID = "footer", //name of your footer element
    nonInteraction = 'true', 
    useGTM = true; //true will send events to GTM data layer

    var maxScroll = 0,
    body = document.body,
    html = document.documentElement,
    footerElement = document.getElementById(footerElementID),
    //change here if you need to report the events in another way (eg the old async GA)
    if (useGTM) {
        reportScrollEvent = function (label, value){
            dataLayer = window.dataLayer || [];
                'event' : 'scroll',
                'eventCategory' : 'scroll',
                'eventAction' : 'scroll',
                'eventLabel' : label,
                'eventValue' : value,
                'nonInteraction' : nonInteraction
    else  {
        reportScrollEvent = function (label, value){
            ga('send', {
                'hitType' : 'event',
                'eventCategory' : 'scroll',
                'eventAction' : 'scroll',
                'eventLabel' : label,
                'eventValue' : value,
                'nonInteraction' : nonInteraction

    return function () {
           note that scrollable height is recalculated each time a 
           scroll event fires - to deal with responsive changes
           if there are concerns about the responsiveness of the page, 
           consider moving this claculation outside to the closure 
           where it will only be done once, rather than on each scroll event
        var bodyH = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight), //the height of the page body
        viewH = Math.max(html.clientHeight, window.innerHeight || 0), //the height of the view
        scrolledH = Math.floor(Math.abs(body.getBoundingClientRect().top)), //the amount scrolled
        //height that can be scrolled. If footer not found, use the height of body, otherwise use body-footer
        scrollableH = (footerElement === null)?(scrollableH = bodyH - viewH):(scrollableH = bodyH - viewH - footerElement.offsetHeight);
        //proportion of possible scroll completed, in 25% increments  
        //note 100% maximum, otherwise can report 125% etc, where we have scrolled into the footer 
        percentScrolled = Math.min(100,Math.floor((scrolledH / scrollableH) * 4) * 25); 

        if (percentScrolled > maxScroll) { //event fires once each time a 25% scroll increment in passed
            maxScroll = percentScrolled;
            reportScrollEvent(percentScrolled + '%', scrolledH);
// ]]></script>

One thought on “Track and measure scrolling

Leave a Reply

Your email address will not be published. Required fields are marked *