#GTMTips: Simple Custom Event Listeners With Google Tag Manager

How to create simple custom event listeners with Google Tag Manager. You can track any user interaction on the site with this solution.

First of all, check out this article for an overview of how custom event listeners work in Google Tag Manager. The reason I’m writing this #GTMTips article is that I want to upgrade the solution slightly, and I want to bring it back into the spotlight. Why? Because it’s still one of the most effective ways to customize your Google Tag Manager implementation.

A custom event listener is a handler you write with JavaScript. It lets you handle any JavaScript DOM events, such as click, form submit, mouse hover, drag, touch, error, page load and unload, and so many more. It also lets you leverage the useCapture parameter which will prove very helpful if you have other JavaScript on the site interfering with GTM’s default event triggers.

Tip 69: Create custom event listeners with ease

The solution comprises the following items: a Custom HTML tag firing with the page load, and a Custom JavaScript variable providing the callback. You’ll also need a bunch of Data Layer variables to fetch the values pushed into dataLayer by the callback.

The idea is that when the page loads, you attach your custom listener to whatever element you want to track for the given event. Then, when this interaction is recorded by the browser, the code pushes an object into dataLayer which you can then use to populate your tags.

The Custom HTML tag

This is what the Custom HTML tag would look like.

  (function() {
    // Use events from https://developer.mozilla.org/en-US/docs/Web/Events
    var eventName = 'dragstart';
    // Attach listener directly to element or document if element not found
    var el = document.querySelector('img#download') || document;
    // Leave useCapture to true if you want to avoid propagation issues.
    var useCapture = true;
    el.addEventListener(eventName, {{JS - Generic Event callback}}, useCapture);

Fire this on a Page View trigger if attaching directly to the document node, or a DOM Ready trigger if attaching directly to an element and the element is in the HTML source, or a Window Loaded trigger if attaching directly to an element that is added dynamically to the page during the page load.

Make sure you change the eventName value to reflect which event you want to track. If you want to track clicks, change it to click. If you want to track users hovering over the element, change it to mouseover, and so on.

You can choose to add the listener directly to an element by using the appropriate CSS selector as the parameter of document.querySelector(). Alternatively, you can add the listener directly on the document node.

Finally, you can set useCapture to false if you want to use the bubble phase instead of the capture phase with your event handler. Because you are simply tracking interactions and not actually creating any side effects, I really recommend leaving this as true.

This is significant especially if you have other JavaScript on the site messing with event propagation. A typical symptom of this is that your Form and Just Links triggers refuse to work. So by using the capture phase, you are evading propagation-stopping JavaScript, and might just be able to track these events when GTM’s default triggers are unable to do so.

The last line actually adds the listener, providing a Custom JavaScript variable as the callback.

The Custom JavaScript variable

Here’s what {{JS - Generic Event callback}} should look like:

function() {
  return function(event) {
      event: 'custom.event.' + event.type,
      'custom.gtm.element': event.target,
      'custom.gtm.elementClasses': event.target.className || '',
      'custom.gtm.elementId': event.target.id || '',
      'custom.gtm.elementTarget': event.target.target || '',
      'custom.gtm.elementUrl': event.target.href || event.target.action || '',
      'custom.gtm.originalEvent': event

This callback pushes a bunch of information about the event into dataLayer, namespacing everything with the custom.gtm. prefix. The event name itself will be custom.event.<event name>, e.g. custom.event.click for a click event or custom.event.dragstart when tracking the dragging action.

The variables pushed into dataLayer mirror those used by GTM’s default triggers, with the exception of custom.gtm.originalEvent which will contain a reference to the original event that invoked the callback. This is significant if you need information from this event object, such as which mouse button was clicked when a click is registered. This is (currently) missing from GTM’s default trigger functionality.

Data Layer variables

You need to create a Data Layer variable for each of the keys pushed into dataLayer. To mimic Google Tag Manager’s naming schema for Built-in variables, you could use something like these:

Variable name Data Layer Variable Name
{{Custom Event Element}} custom.gtm.element
{{Custom Event Classes}} custom.gtm.elementClasses
{{Custom Event ID}} custom.gtm.elementId
{{Custom Event Target}} custom.gtm.elementTarget
{{Custom Event URL}} custom.gtm.elementUrl
{{Custom Event Original Event}} custom.gtm.originalEvent

The Custom Event trigger

To fire your tags when a custom event is registered, you’ll need a Custom Event trigger set to the event name pushed into dataLayer in the Custom JavaScript variable callback. So, to follow the example of the dragstart event (registered when the user starts dragging the given element in the browser), the trigger would look like this:

Working example

Let’s tackle a problem that you might well have on your site. You want to track a form element with id contactUs, but no matter what you do, GTM’s own Form trigger refuses to fire. You’ve looked around, read my articles, and come to the conclusion that the problem is other JavaScript on the site stopping the propagation of the form submit event. Your friendly local developer tells you that due to the nature of the plugin you use, it’s impossible to change this behavior.

Custom event listeners to the rescue! You can trust the useCapture flag to track the form submission even though propagation has been stopped. Here’s what the Custom HTML tag would look like:

  (function() {
    // Use events from https://developer.mozilla.org/en-US/docs/Web/Events
    var eventName = 'submit';
    // Attach listener directly to element or document if element not found
    var el = document.querySelector('form#contactUs') || document;
    // Leave useCapture to true if you want to avoid propagation issues.
    var useCapture = true;
    el.addEventListener(eventName, {{JS - Generic Event callback}}, useCapture);

Add a DOM Ready trigger to this tag, set to fire on pages which have this particular form in the HTML source.

Now, whenever a form submission is detected, your Custom HTML tag will go off, pushing the following object into dataLayer:

    'event': 'custom.event.submit',
    'custom.gtm.element': form#contactUs,
    'custom.gtm.elementClasses': '',
    'custom.gtm.elementId': 'contactUs',
    'custom.gtm.elementTarget': '',
    'custom.gtm.elementUrl': 'https://www.domain.com/my-form-handler/',
    'custom.gtm.originalEvent': submitEvent

And you can pick these up with the Data Layer variables you created earlier.

You’ll just need a Custom Event trigger, where the Event name field has the value custom.event.submit.

Once you attach that trigger to your tag, you can use the Data Layer variables to populate all the relevant fields.


Whenever I talk about GTM to someone, and that’s very often, I always end up talking about the many ways we can customize Google Tag Manager to work even more efficiently on our websites. Custom Event listeners are still, after all these years, my favorite way of customizing a GTM setup.

They give you so much power in tracking user interactions on the site.

As always, I hope GTM continues to release new default triggers for us. I’ve long dreamed of a “blank” event trigger, where you simply have to add the DOM event name, and it would also have a checkbox for whether you want to use capture mode or not. It would make this custom solution redundant, but that’s only a good thing in my book.

Have you created any creative custom event listeners in your GTM setups?