Form Engagement Tracking with Google Tag Manager



Do you know how people are completing forms on your site? Are there certain fields that get skipped frequently or that cause users to drop off?

Almost two years ago, I wrote a post showing how to use a simple script to track form abandonment in Google Analytics with event tracking. I’ve gotten a lot of great user feedback (and requests) about that script, and wanted to share an updated version that is a little more elegant.

This new version more effectively handles fields that are completed or skipped. I’ve also modified this script and included instructions for how to add it to your site through Google Tag Manager.

Use this script to see which fields get the most completions, but also use it to compare to the amount of forms that get submitted successfully. If you find that people are starting to complete the form but failing to submit it, you may need to look into ways to improve the user experience.

The updated script

What does this script do?

This is a simple listener script that uses jQuery to “listen” for certain events on the page. Specifically, it listens for a user’s cursor to exit an input field, either by tabbing to the next field or clicking out of the field.

When that happens, this script checks to see if any text was entered. If there is text, it pushes an update to the data layer that includes a category with the name of the form, an action of “completed” and a label that is set to the name of the form field.

Note: This process of using a data layer push to trigger a generic event can be reused for other tracking opportunities!

If the field is left blank when the cursor exits, it fires a similar event, but sets the action field to “skipped”.

What’s new?

The biggest change is that I’ve built in some logic to only fire an event once per field completion and once per field skip. With the original script, if a user’s cursor entered and exited a form field 10 times, it would fire 10 events to Google Analytics. Now, there will be at most 2 events fired per form field (one if they skip a field and a second if they complete it).

The next change is that this script is updated to work with Google Tag Manager. Instead of firing an event directly into Google Analytics, we’re now pushing that information to the dataLayer, and using Google Tag Manager to handle the event tracking.

Here are the steps to make this work in GTM:

Step 1:

Create Custom HTML Tag named Listener – Form Fields. Copy and paste the above script into this tag. The rule/trigger to fire this tag can be set to All Pages if you have forms across many pages of your site.

Alternatively, you could create a new rule to only fire this tag on pages where you have forms that you want to track, like the screenshot below. In that case, your rule may look like {{url path}} matches RegEx page1|page2|page3.

form pages rule

Step 2:

Create macros/variables that record the values from the script that are pushed to the dataLayer for event category, event action, and event label. Create three data layer variable macros that pull the values that we’re passing from the dataLayer. Create one for “eventCategory”, “eventLabel”, and “eventAction.”

event category macro

Step 3:

Create a new Google Analytics tag and name it GA – Event. You’ll need to fill in the appropriate property ID. Choose the track type of Event, and use the following settings:

event tag

Step 4:

Create a rule/trigger that listens for the custom event that we’re passing from our script. Use this as the Firing Rule for the Tag we just created.


Now that everything is set up, you can preview and debug these new tags and macros to make sure they’re working as expected without any conflicts. Then create a new version of the container and publish!

Things to keep in mind:

  • Just like the old version, this script requires jQuery. Make sure you include jQuery before your GTM container script (i.e. in the <head> section of your page).
  • This script pulls in the name attribute of the form as well as each of the form fields. It uses these values for the event category and label, respectively. Make sure you use descriptive names instead of something like field1 or field2. If you already have descriptive IDs on each form field, you could use those instead. Just replace $(this).attr(‘name’) with $(this).attr(‘id’) in the form tracker script.
  • You may also want to track submissions of the form. This can be done easily with a form submit listener tag in GTM. This form tracking basics in GTM post will help explain the basics (as well as advanced topics) of tracking form submissions.

Jim Gianoglio is a Senior Analytics Consultant. He works with implementation, analysis and training of Google Analytics and Google Tag Manager. Before focusing on analytics, he led the SEO campaigns of Fortune 500 companies in the insurance, retail and CPG industries. Things you didn’t know about Jim: he’s biked from Pittsburgh to Washington DC in 41 hours, roasts coffee beans and has done voiceovers for TV commercials.

  • Christopher

    @Jim Gianoglio

    Sweet! thanks going to try this out now 🙂

  • Gerard

    question: I’ll get Form- for event category.

    Could I change this only in the html of the site or also in your custom html script?

  • Jim Gianoglio

    @Gerard –

    The event category will be “Form – ” followed by whatever the is in the name attribute of your form. For example, if you have:

    Then the event category would be “Form – Sign Up Form”.

    You could change the html of your page to change what comes after the “Form – ” part, or you could change the code above to be something different. You could hard code it to be a specific string of text, or you could use the form ID instead of the name.

  • Mariana Alves

    I am very grateful for this post!

    I was looking for this for a long time.

    I will try to use and once again thank you very much.

  • Ronny

    Hi i tryed to copy your code to my website. But in Step 3 i dont know what to do. When i push the button create a new tag … i have to decide which tag typ i want to use. there are many but nothing option like in your picture. can you show me the way plz ? thx

  • Gerard

    Thank you for your response. One question:

    Could you track error alerts in your forms with this script?

  • Ronny

    Hello thanks found it. when i inculded all steps and the code to the website… i see the events in google analytics ? like in your first code

  • Jim Gianoglio

    @Gerard –

    Error alerts could be tracked, but it depends on how your form is set up and how the validation occurs (server-side, client-side, or both). There’s no easy way to just write a script that applies to all possible forms, unfortunately.

    The high-level view of how I would do track error alerts would be to modify the script that handles the validation to push information to the dataLayer when the form doesn’t validate. The information you push could include which fields had errors, what the error message was, etc. For example:

    // when form doesn’t validate
    ‘event’: ‘formError’,
    ‘errorFields’: ‘field1-field2-field3’,
    ‘errorMessage’: ‘missing-wrongFormat-invalidAddress’

    The above example would be more likely with server-side validation, where it might return multiple errors at once. With client-side validation, you could send the errors one field at a time as they happen.

    But again, this would be different from one form/site to the next.

  • Peter

    my english isnt the best but i will try:)

    i implement your form Form Engagement Tracking successfully to my form.(he send the right stuff to ga)

    I have 3 Step buying “shop”. First you fill out what you want(1.html) then you fill out the form with your personal stuff(2.html) and after that u will get a thank you site(3.php). With the last site all your date will be upload to the sql database.

    The Problem is when i active google tag manager on the personal stuff site(2.html), the costumer will not be forwarded to the thank you site(3.html). He goes back to the first site (1.html).

    I dont know why this happend.

    Have you an adivce for me ?
    my site(2.html) is

  • Peter

    PS: when i turn off the Listener – Form Fields everything works… do you have an idea ? thank you

  • Jim Gianoglio

    @Peter –

    It’s hard to say without digging into the details of your form. But since this script uses jQuery listeners, it’s possible that they are conflicting with existing listeners that make your form work.

    It is odd that the form submit is what’s being affected, since this script isn’t listening for form submits – just when the mouse leaves a form field.

    This is a good example to always remember to test first before publishing a container in GTM.

  • James

    Hi Jim,

    This is exactly what I was looking for so thank you for that.

    Quick question, do I need to set this up through GTM or can I just copy into my pages your script and it will start working in Google Analytics?

    Many thanks again for a great script.

    Kind regards,

  • Jim Gianoglio

    @James –

    The code in this post is specific to GTM, so you can’t just copy/paste it as is.

    If you’re not using GTM, you can make the following changes to the code, and then just past it into your page (beneath jQuery):

    dataLayer.push({‘eventCategory’: ‘Form – ‘ + $(this).closest(‘form’).attr(‘name’),
    ‘eventAction’: ‘completed’,
    ‘eventLabel’: $(this).attr(‘name’),
    ‘event’: ‘gaEvent’});

    ga(‘send’, {
    ‘hitType’: ‘event’,
    ‘eventCategory’: ‘Form – ‘ + $(this).closest(‘form’).attr(‘name’),
    ‘eventAction’: ‘completed’,
    ‘eventLabel’: $(this).attr(‘name’)

    And change:
    dataLayer.push({‘eventCategory’: ‘Form – ‘ + $(this).closest(‘form’).attr(‘name’),
    ‘eventAction’: ‘skipped’,
    ‘eventLabel’: $(this).attr(‘name’),
    ‘event’: ‘gaEvent’});

    ga(‘send’, {
    ‘hitType’: ‘event’,
    ‘eventCategory’: ‘Form – ‘ + $(this).closest(‘form’).attr(‘name’),
    ‘eventAction’: ‘skipped’,
    ‘eventLabel’: $(this).attr(‘name’)

  • James


    Thank you so much, absolutely fantastic.

    Thanks for your help.


  • Danielle Wooding

    Hi Jim – This is fantastic. I’ve been trying to work this out on my own, so you’ve saved me a lot of time.

    Question: I’ve changed the code a bit to accommodate for our particular setup which doesn’t currently use the name attribute. For the form I’m pulling from the action attribute which returns the form URL with a query string which I’m removing using the split function.

    var formString = $(this).closest(‘form’).attr(‘action’);
    var formName = formString.split(“?”)[0];
    dataLayer.push({‘eventCategory’: ‘Form – ‘ + formName, ……

    So far so good, but I have 3 types of fields on my form, each of which has a different attribute that I want to pull the label from and I can’t seem to figure out how to create the proper if/else statement or OR condition within the current code. So for example, the first 5 fields are text fields where I want to use the placeholder attribute, the next field is a droplist where I’d like to use the class, and the last one is a checkbox where I’d like to use the value.

    Any advice?

  • Tiaan Van Zyl

    Hi Jim,

    Thank you for the solution to a question I had. I implemented it according to your instructions, it is working perfectly.

    I have, however changed the non-interaction to “False” as I want to see which field are clicked on and those that are not. Is this a correct method?

    I am receiving “Form – undefined” in the event category. I tried to insert the form name (Contact Page) to the HTML script, but undefined remained there, but is pushed to the back of the name i.e. Form – Contact Usundefined”. Do you have a solution for this?

    Thank you

  • Gerard

    Could you use form error tracking in a custom html tag:

    // when form doesn’t validate
    ‘event’: ‘formError’,
    ‘errorFields’: ‘field1-field2-field3′,
    ‘errorMessage’: ‘missing-wrongFormat-invalidAddress’

    Or should you implement this data-layer hard coded in the shopping cart?

  • Nick

    Hi Jim,

    awesome, i was looking for this a long time.

    Unfortunately, like Ronny i get stuck in step 3. How exactly do I create an event tag? I am using the german version of GTM. It offers “event listener” with several sub-options, but none of them looks like step 3 of your walk-through.

  • Hadi

    Hi Jim,

    It is an awesome script.
    I have tried it and it works perfectly.
    However, I have a question regarding to this solution. Let’s say that I have 2 forms which has 10 fields each then I will need to create 2(forms)x10(fields)x2(state:completed and skipped) goals in GA? Is it correct? If I am not wrong that we only permitted to have 20 goals for each GA account.

    Can you please give me some lights on how to address this?

    Thank you.

    Best Regards,


  • Pablo Palacios Fiestas

    Hi, When I use this script I can see with the Preview on TagManager this event are completed. But, when I see Real time on GA that event not appear. What’s wrong?

    • Jo Santana

      Same issue here. 🙁

      • Jim Gianoglio

        Real Time reports can be fickle when it comes to events, from my experience. Check your standard event reports and see if they show up in a few hours.

    • Nakedi Sugar

      check if your filters on GA are not excluding your IP address

  • Peter Risman

    thanks so much for posting this script – it works great. i have a follow-up question: how do I track the length of time that elapses between Page View and Form Submit (on the same page)?

    i haven’t been able to figure it out myself, nor have I been able to find an online example .

    • Jim Gianoglio

      Hey Peter –

      You could use either event tracking or user timings to track the amount of elapsed time between the page load and the form submit. But you need a way to keep track of how much time has elapsed – a counter.

      For example, you could use some JS to help with this, like:

      var start = new;
      var elapsed = new – start;

      Thinking about it at a high level, you could have a variable (let’s call it {{gtm start}}) that grabs the current time when the pageview fires. There’s a built in dataLayer variable to do this – gtm.start. So create a new variable, choose the Data Layer Variable type, and for the Data Layer Variable Name type in gtm.start. This variable will the time (in milliseconds since Jan. 1, 1970) that the pageview event occured, which is the same as

      Then, You could could create a custom JS variable (let’s name it {{elapsed time}})that takes the current time and subtracts the start time:

      function() {

      var startTime = {{gtm start}};

      var currentTime = – startTime;

      return currentTime;


      Remember that variables are evaluated whenever there’s an event in GTM (gtm.js, gtm.dom, gtm.load, gtm.formSubmit, gtm.linkClick, etc., or custom events that are pushed to the dataLayer).

      You could use this {{elapsed time}} variable in an event tag that gets fired on form submit, or you could even use User Timings (

      Hopefully this gives you enough direction and ideas.

  • Phill George

    The optional event tracking parameters in steps 3 appear to be invalid. When i tried to use them. I had to switch {{event category}} to {{eventCategory}} and the same for the other 2. is this right ?

    • Jim Gianoglio

      The event tracking parameters in step 3 just need to match whatever you named your variables/macros in step 2. If you follow the example exactly, then you would use {{event category}}.

  • Jim Gianoglio

    Hadi –

    You are correct that you only have 20 goals per view. But you don’t need to track each form field as a goal. I’d recommend tracking the form submit of each form as a goal (so in your case, you’d have 2 goals).

  • Cristina

    Thank you for writing this blog.
    I set up everything as explained above in GTM and after the website tag. In the preview and debug version I see that the Data Layer values are not empty. See below:

    gtm: {start: 1445527209782, uniqueEventId: 1445527210072},
    event: ‘gaEvent’,
    eventCategory: ‘Form – campaigns_form’,
    eventAction: ‘skipped’,
    eventLabel: ‘phone’


    The problem is that in Google Analytics I see in the event overview tabs the Event Action, Event Label, Event Category as “undefined”. Do you know why is tis happening and how to fix it? Thank you in advance for your help!

    • Jim Gianoglio

      Hi Cristina,

      Did you set up variables to store the values for eventCategory, eventAction, and eventLabel? Check your Google Analytics event tag and make sure you’re referencing those variables exactly as you set them up. Variables are case sensitive, so if you have a variable named event category but you reference {{Event Category}} it will return undefined.

      Also, check your trigger for the Google Analytics event tag to make sure it’s firing on the appropriate GTM event (step 4 from the post).

  • Jsmirror

    I have tested and also set eventCategory, eventAction, and eventLabel as vaiable ID’s.
    As Debugger shows 0 errors it is correctly linked to Trigger (Custom Event) ID gaEvent.

    I’m not sure but testing values and DLV it will be still pushed Form – undefined.

    Due to jQuery validation I have also set var $ = jQuery;

    V2 combines new settings (see trigger). Can you publish all settings as it seems it will be missed other.

    • Jim Gianoglio

      I have this on my list of blog posts to update – hopefully that will clear up some of the confusion between V1 and V2 differences.

      Thanks for the feedback!

  • Levente K Szabó

    Can this be added to WooCommerce checkout form as well?


    • Jim Gianoglio

      Yes – this should work with WooCommerce. The form fields with WooCommerce (at least in one of my own implementations) have pretty useful attributes. For example, they have name and ID attributes like billing_first_name, billing_email, shipping_address_1, etc.)

  • elsmore01


    Has this been used successfully with the latest version of GTM? I can see the
    events in GTM preview mode but I simply cannot get the data passed to Analytics
    – it just shows as “undefined” as Cristina sees below. The interface has changed since this article was published – screens attached to illustrate.

    • Jim Gianoglio

      Hi elsmore01 –

      Check my response to Cristina’s comment. Double check that you have a Google Analytics event tag that is set up to send your variables (event action, event category, etc.), and make sure it has a trigger like in step 4.

      Also, you need to have event category and event action (those are both required fields) – otherwise, the hit won’t be sent to GA.

      • elsmore01

        Thanks for your reply, Jim. I have checked / recreated these over and over – the 4th image I posted shows my GA event tag, and the 3rd image shows my trigger. Images 1,2,5 show my variables. As mentioned the interface is different, so perhaps I have filled something in incorrectly? It’s seems like this should be pretty straightforward though, which is why I wondered if the technique is still valid.

        • Jim Gianoglio

          Ahaa! I didn’t see the images before (they were hidden behind a “Show More” Link).

          You have set up the trigger correctly, but your Google Analytics event tag has the “All Pages” trigger applied to it. Remove the “All Pages” trigger, and replace it with your “Event is gaEvent” trigger, and you’ll be good to go!

          • elsmore01

            Yes! Thanks Jim, that’s done it. Really appreciate your help.

  • Evan Crean

    Hi Jim,

    Thanks for posting this script. I’ve implemented it in GTM and it works great. However I’ve run into an issue with my particular form where the same fields are firing events for being ‘skipped’ and ‘completed’ when they shouldn’t be.

    When a field is in its natural state it has just has a class of “scfSingleLineTextLabel” (see screenshot 1).

    But when I click or tab into the field, its class immediately updates to “scfSingleLineTextLabel skipped” before any content is entered or the field is actually skipped (see screenshot 2).

    Then when I actually fill out the field and move on to another one, the class updates to “scfSingleLineTextLabel skipped completed”. This results in ‘skipped’ and ‘completed’ events firing in GTM/GA.

    Do you have any recommendations on how I could update the script to combat this issue or ways that I can work around it with GTM and GA? Thanks!


    • Jim Gianoglio

      Hi Evan –

      Can you share a link to the form? It’s hard to say what’s causing this without more information. Is it only this one particular field that is behaving this way, or is it all form fields? What type of field is it (e.g. text, dropdown, radio button, checkbox, etc.)?

      • Evan Crean

        Hi Jim,

        Thanks for your speedy reply. Is it okay if I email you a link to the form since it’s for a client? All of the form fields are behaving this way including a dropdown form.


      • Evan Crean

        Also, if you’d prefer I could send it via a Twitter DM. Please let me know the best route to provide you with additional information. Thanks Jim!

  • Jeffrey Bleijendaal

    Great post Jim. Thank you so much. I’ve implemented it step-by-step and also checked the comments/questions below concerning the new layout of GTM. Everything should be right, but nothing is brought into the datalayer. Everyting is ’empty’ and most of the standard variables are ‘undefined’. I have no idea what can be wrong and I hope you can help me in some way.

  • Oscar

    I do not see the option to follow step #2 (Create macros/variables that record the values from the script that are pushed to the dataLayer for event category) Is there a new version of GTM where macros have been removed? Am I missing something?

    • Jim Gianoglio

      Hi Oscar –

      Sorry – this was written back before V2 of Google Tag Manager came out. With V1, we had Macros instead of Variables (and Rules instead of Triggers). The “old” Macros and the “new” Variables are exactly the same – so for step 2, you’ll just need to create a user-defined Variable following settings shown in step 2.


      • Oscar

        Thanks for your quick and detailed response Jim. I’ll work on this and get back to you when I’m able to successfully fire up this events 🙂

  • Florian

    Hi Jim,
    Thank you for your instructive post.
    I´m trying to use this method for one of my clients but I can´t see any datas in the GA interface neither in the Tag Assistant (as you can see in the attachement below). My client´s website get the GA Universal tag externalized in a JS called in the . Is it possible that this method prevents the Data Layer to get the event values and then no trigger the script?
    Thank you for your help.

    • Jim Gianoglio

      Hi Florian,

      Having the GTM script loaded via an externally included JS file is fine. If it’s in the , just make sure the portion of the container code is not included (or your HTML will not validate).

      However, if you are saying that the client has GA hard-coded on the site (i.e. loading the analytics.js library manually instead of through GTM), then that could cause issues.

      However, either way, you should still see information being pushed to the dataLayer. I suspect there’s something else at play that’s preventing the tag from firing.

  • Chris Seal

    Hi Jim,
    I’ve been trying to implement this for a little while, to the point where I’ve started again. Would really appreciated your assistance, as I don’t know anyone else who has done this who might be able to spot where I’ve gone wrong.
    Screenshots attached. If you need more info, please do let me know! Much appreciated 🙂

    • Jim Gianoglio

      Hi Chris,

      One thing you’ll need to fix is that you’re using a Classic Google Analytics tag for the event. This should be a Universal Analytics tag type (to match your pageview tag).

      I suspect the other issues are probably related to how this script grabs the form name and field names. Look at lines 9, 11, 22, and 24 of the script. It references the “name” attributes of the form and form fields. Check the source code for your form. If your form doesn’t have a name attribute, or your form fields don’t have name attributes, this will not work and you’ll need to edit that code at the lines mentioned to reference something in your form that does exist (look for an id or class that identifies the form and/or form fields).

      Hope that helps!

      • Chris Seal

        Hi Jim,

        Thanks for the speedy response! 🙂

        Apologies, I did already have it as a UA tag, and tried it as a classic GA tag to see if it made a difference 🙁

        Will double-check the form attributes.

        Much appreciated,

  • Michael

    Hi Jim! Great script and guide! Is it possible to change the script so that it doesn´t validate when the formfield isn’t accurate
    eg. if you miss @ in the email-field. How would that look like?

    • Jim Gianoglio

      Hi Michael,

      Yes – that can certainly be done! You’d just need to modify the first “if” statement in the script above. So instead of :
      if($(this).val().length > 0 && !($(this).hasClass(‘completed’))) {….

      You’d have something like:

      if($(this).val().length > 0 && !($(this).hasClass(‘completed’)) && $this.val().indexOf(“@”) !== -1) { …

      Keep in mind, that making just this change to the script would mean that any field that is not an email field would never “validate” and the “completed” event would never fire. So if you have other fields in your form that aren’t email fields, you’d need separate “if” statement(s) for them. Depending on your form, and exactly what you want to track, there may be a more elegant, custom solution that would work better.

  • Stefan

    Thanks Jim for this script and guide!

    Just a quick note regarding the script for anyone who is using dynamically loaded forms, it should be made clear that they need to ensure that the HTML script fires after the forms have been loaded into the page. This could be on Window load, or a custom event (set up by the web devs).

    • Jim Gianoglio

      Thanks Stefan! That’s very good advice, and it’s worth pointing out that form come in many different varieties and flavors, and creating a one-size-fits-all solution just isn’t feasible. For example, some forms update dynamically as you make selections along the way, and these have to be dealt with is a specific way.

      Thanks again for your comment!

Contact Us.


24 S. 18th Street, Suite 100,
Pittsburgh, PA 15203

Follow Us



We'll get back to you
in ONE business day.