Friday, February 21, 2025
HomeAnalyticsTrack Interactions In The Shadow DOM Using Google Tag Manager

Track Interactions In The Shadow DOM Using Google Tag Manager


when it actually landed on the element within #shadow-root. Similarly, because the shadow DOM’s contents are hidden from the parent structure, the matches CSS selector predicate can’t be used to see what’s inside the clicked element.

We can work around this! We can’t use GTM’s built-in triggers as they don’t let us access the event object itself. But we can use a custom event listener.

For more details on how event handling within the shadow DOM works, take a look at this excellent article on the topic.


X


The Simmer Newsletter

Subscribe to the Simmer newsletter to get the latest news and content from Simo Ahava into your email inbox!

How event handling works with the shadow DOM

Event listeners within the shadow DOM work just like event listeners on regular DOM structures. An event is registered, and it populates a path through the layers of the site during the capture and bubble phases.

Some events bubble up towards the top of the DOM tree, some stay on the node where the event was registered.

The main difference with the shadow DOM is that events that start making their way up only cross the shadow DOM boundary if they have the property composed set to true.

Most events have composed set to true. Typically, the exceptions are events that are not based on UI interactions. Like these:

For events that have composed set to true, we can attach a custom event listener on the document node, for example, and the events that take place within the shadow DOM will propagate to our listener (assuming they also bubble, or the listener has been set to detect the capture phase instead).

However, we are still faced with the problem introduced in the beginning of this article. All events that take place in the shadow DOM are auto-delegated to the parent of the #shadow-root. This isn’t very helpful. The shadow DOM could be a huge, sprawling thing, so we need precision.

Luckily, we can use the Event.composedPath() method to get an array that represents the path the event took as it bubbled up. The very first member in the array is the item that was actually clicked (unless the shadow DOM was closed, but we’ll get back to that in a minute).

We can use this information to build our listener.

The listener

In Google Tag Manager, create a Custom HTML tag, and type or copy-paste the following code.

script>
  (function() {
    // Set to the event you want to track
    var eventName = 'click',
    // Set to false if you don't want to use capture phase
        useCapture = true,
    // Set to false if you want to track all events and not just those in shadow DOM
        trackOnlyShadowDom = true;

    var callback = function(event) {
      if ('composed' in event && typeof event.composedPath === 'function') {
        // Get the path of elements the event climbed through, e.g.
        // [span, div, div, section, body]
        var path = event.composedPath();
        
        // Fetch reference to the element that was actually clicked
        var targetElement = path[0];
        
        // Check if the element is WITHIN the shadow DOM (ignoring the root)
        var shadowFound = path.length ? path.filter(function(i) {
          return !targetElement.shadowRoot && !!i.shadowRoot;
        }).length > 0 : false;
        
        // If only shadow DOM events should be tracked and the element is not within one, return
        if (trackOnlyShadowDom && !shadowFound) return;
        
        // Push to dataLayer
        window.dataLayer.push({
          event: 'custom_event_' + event.type,
          custom_event: {
            element: targetElement,
            elementId: targetElement.id || '',
            elementClasses: targetElement.className || '',
            elementUrl: targetElement.href || targetElement.action || '',
            elementTarget: targetElement.target || '',
            originalEvent: event,
            inShadowDom: shadowFound
          }
        });
      }
    };
    
    document.addEventListener(eventName, callback, useCapture);
  })();
script>

You can attach a Page View trigger to this tag. After that, every single click on pages where the listener is active will be pushed into dataLayer with an object content that looks like this:

In this case, the click fell on a

with very few attributes, but which was contained in the shadow Dom (as isShadowDom is true).

You can then create a Custom Event trigger for custom_event_click:

And you can create Data Layer variables for the individual items in the pushed object like this:

By switching eventName to, say, submit, you can listen for form submissions instead.

If you want to avoid having the script push a message with every single event instance, you can add checks within the callback that verify the event target was a specific type of element. For example, to only push to dataLayer if the click landed on a link, you could do something like this:

var callback = function(event) {
  ...
  var targetElement = path[0];
  if (targetElement.matches('a, a *')) {
    // Run the dataLayer.push() here
  }
  ...
...

Note! Though it was just an example, you should be aware that .matches() won’t work in IE, and you’ll need to use .msMatchesSelector().

What about non-composed events?

What if you want to track events that don’t have the composed flag set to true? If you remember, those events will not propagate past the shadow DOM boundaries. Similarly, if you use the script above, they will also have the inShadowDom flag set to false, as they are practically oblivious to the fact that they are in a shadow DOM (Matrix-style).

So, you’ll have to do event handling without the power of delegation. In other words, you’ll need to add the listeners directly to the elements.

For example, if you wanted to track a load event for a

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments