GTM Universal Click - No JavaScript

Universal Click Tracking in Google Tag Manager — A Complete Recipe Without Custom JavaScript

Capture rich click context on every element — including deeply nested components — using only GTM's built-in variable types and sandboxed templates. No custom JS. No new CSP hashes.

Published · Tags: GTM, Google Analytics 4, Click Tracking, dataLayer, aria-label, data-testid, CSP, GA4

Tracking clicks in GTM sounds simple until you're staring at a modern React or Angular component where the element that actually receives the click is three, five, or even ten DOM levels deep inside the component that carries the only meaningful identifier. GTM's built-in {{Click ID}} and {{Click Classes}} variables see what was clicked — not necessarily what it belongs to. This recipe solves that problem entirely within GTM's native variable system, with no Custom HTML tags and no custom JavaScript that would require new Content Security Policy SHA-256 hashes.

⬇ Download the GTM Container JSON from Google Drive

What's Inside the Container

The exported container (universal-click-tracking-recipe.json) is organized into two folders of assets, plus a suite of built-in variables and two community Custom Templates from Sublimetrix.

Folders

FolderContains
Analytics GA4 configuration tag, shared settings variables, environment/stream routing variables, and the browser Client ID utilities.
Clicks The All Clicks Send Event tag, the All Clicks All Elements trigger, and all click-scoped variables — coalesce chains, DOM traversal variables, URL decomposition, and PII filters.

Tags

TagTypePurpose
Google Analytics ConfigurationGoogle Tag (googtag)Standard GA4 config tag wired to environment-aware Stream IDs via shared configuration and event settings variables. Fires once per load.
All Clicks Send EventGA4 Event (gaawe)Fires a GA4 click event on every click, forwarding the full set of enriched click parameters.

Trigger

TriggerTypePurpose
All Clicks All ElementsClick — All ElementsFires on every click on the page with no additional filter conditions. The enrichment happens in the variables, not the trigger.

Custom Templates (from Sublimetrix)

TemplatePurpose
If Else If — Advanced Lookup TablePowers the coalesce variables — walks the DOM ancestor chain and returns the first non-empty, non-junk value found. Used for id, className, aria-label, and data-testid.
Page Path — Part NDecomposes the click URL path into its segments. Used by the Click URL Document variable to extract the last path segment (the filename or slug of the clicked link's destination).

The Core Problem: Nested Elements and Missing Identifiers

In component-driven UIs, a click on a card, button, or navigation item frequently lands on an inner <span> or <svg> node that has no id, no meaningful class, and no visible text. GTM's automatic click variables ({{Click ID}}, {{Click Classes}}, {{Click Text}}) see only the immediate target. The component's semantic identifier — the one your developers actually used to name the thing — lives on a parent or grandparent element several levels up.

This recipe handles that by maintaining a set of DOM traversal variables for each attribute of interest, walking up to ten levels of ancestor chain. A coalesce variable then picks the first non-empty value found at any level, returning it as a single clean parameter to GA4. No JavaScript is needed: GTM's built-in Data Layer Variable type can traverse gtm.element.parentElement.parentElement… paths natively.

Why this matters for CSP: Custom HTML tags that include <script> blocks require you to generate and maintain SHA-256 hashes in your Content Security Policy every time the tag changes. By using only native variable types, Data Layer Variables, and sandboxed Custom Templates, this recipe introduces zero new CSP requirements.

Architecture Overview

Here's how a click travels from the page to GA4:

  1. User clicks anything on the page. The All Clicks All Elements trigger fires immediately — it has no filter conditions.
  2. Built-in click variables are populated. GTM automatically sets {{Click Element}} {{Click ID}} {{Click Classes}} {{Click Text}} {{Click URL}} {{Click Target}} and {{Click Alt Text}} on the gtm.element object.
  3. DOM traversal variables are evaluated. For each attribute family (id, className, aria-label, data-testid), a ladder of Data Layer Variables reads the value at gtm.element, then .parentElement, then .parentElement.parentElement, and so on up to ten ancestors.
  4. Coalesce variables fire. The If Else If — Advanced Lookup Table template checks each level in order and returns the first value that does not match ^(undefined|null|0|true|false|NaN|)$. This produces four single clean values: {{coalesce id}}, {{coalesce className}}, {{coalesce aria-label}}, and {{coalesce data-testid}}.
  5. Supporting variables are computed. {{Click Text Filter}} masks phone numbers and email addresses from the click text. {{Internal or External Link}} converts the boolean {{Is Outbound Link}} to the strings internal_link or external_link. {{Click URL Document}} extracts the last path segment of the clicked URL using the Page Path Part N template.
  6. The GA4 event fires. The All Clicks Send Event tag sends a click event to GA4 with all enriched parameters as event parameters.

DOM Traversal: How the Ancestor Ladder Works

GTM's Data Layer Variable type can follow dot-notation paths into the gtm.element object that is automatically populated on every click. A variable reading gtm.element.parentElement.parentElement.attributes.aria-label.value will return the aria-label value of the grandparent of the element that was clicked — with no JavaScript required.

This recipe maintains three families of such variables, one per tracked attribute, each going up ten ancestor levels:

data-testid ancestor ladder

Variable NameData Layer Path
DOM data-testidgtm.element.attributes.data-testid.value
DOM parent data-testidgtm.element.parentElement.attributes.data-testid.value
DOM grandparent data-testidgtm.element.parentElement.parentElement.attributes.data-testid.value
DOM g-grandparent data-testid…parentElement × 3…attributes.data-testid.value
DOM g-g-grandparent data-testid…parentElement × 4…
DOM g-g-g-grandparent data-testid…parentElement × 5…
DOM g-g-g-g-grandparent data-testid…parentElement × 6…
DOM g-g-g-g-g-grandparent data-testid…parentElement × 7…
DOM g-g-g-g-g-g-grandparent data-testid…parentElement × 8…
DOM g-g-g-g-g-g-g-grandparent data-testid…parentElement × 9…
DOM g-g-g-g-g-g-g-g-grandparent data-testid…parentElement × 10…

The same ladder exists for aria-label (eleven variables: DOM aria-label through DOM g-g-g-g-g-g-g-g-grandparent aria-label), for element id (eleven variables), and for className (eleven variables).

Design note: GTM returns undefined — as the literal string "undefined" — when a path resolves to a node that doesn't exist. The coalesce comparator ^(undefined|null|0|true|false|NaN|)$ catches this, along with other falsy-but-stringified JavaScript values, so they are never mistakenly returned as a useful value.

The Coalesce Variables: Picking the First Meaningful Value

Each coalesce variable uses the If Else If — Advanced Lookup Table template (by Sublimetrix). The rule table works as a priority-ordered fallback: it checks whether the innermost level's value is a valid, non-junk string. If it is, that value is returned immediately. If not, the next row checks the parent level, and so on. The first matching row wins.

Here is the effective logic of {{coalesce aria-label}}, expressed as pseudocode:

if DOM aria-label doesNotMatch "^(undefined|null|0|true|false|NaN|)$"
  return {{DOM aria-label}}
else if DOM parent aria-label doesNotMatch "^(undefined|null|0|true|false|NaN|)$"
  return {{DOM parent aria-label}}
else if DOM grandparent aria-label doesNotMatch "^(undefined|null|0|true|false|NaN|)$"
  return {{DOM grandparent aria-label}}
... (continues up 10 levels)
else
  return undefined  (no default value set)

The same pattern applies to {{coalesce data-testid}}, {{coalesce id}}, and {{coalesce className}}. All four are sent as GA4 event parameters.

Why aria-label and data-testid?

aria-label and data-testid attributes are a natural complement to GTM's automatic click variables for two distinct reasons.

aria-label is a first-class accessibility attribute that provides a human-readable description of an interactive element, especially when visible text is absent (icon buttons, image links, navigation landmarks). It is authored by developers specifically to describe what an element does, which makes it an ideal click label for analytics: it carries semantic intent, not just DOM structure. Because it is part of the HTML spec and reviewed in accessibility audits, it tends to be stable and meaningful.

data-testid is the attribute QA engineers use to uniquely identify elements in automated tests. Teams that practise test-driven development apply it rigorously and keep it stable across UI refactors — exactly the persistence property you want from a click identifier. In sites that use a testing framework such as Cypress, Playwright, or React Testing Library, data-testid is often the most reliable stable identifier available, more so than dynamically generated IDs or utility class names.

Together, these two attributes allow analytics teams to collaborate with developers on a simple instrumentation contract: if you want this element tracked cleanly, add an aria-label or data-testid to the component's root element. GTM does the rest with no further tag changes or CSP work.

Supporting Variables

Click Text Filter

GTM's built-in {{Click Text}} can capture PII if users click on links or buttons that display a phone number or email address in their label. The Click Text Filter variable wraps {{Click Text}} in a Regex Replace variable with three patterns, masking matches with ' *** ':

PatternMatches
\([0-9]{3}\)\s[0-9]{3}-[0-9]{4}US phone numbers in (NNN) NNN-NNNN format
[\w\-\.\\_]+@([\w\-\.\\_]+\.)+[\w\-\.\\_]{2,4}Email addresses
[0-9]{3}-[0-9]{3}-[0-9]{4}US phone numbers in NNN-NNN-NNNN format

The click_text GA4 parameter uses {{Click Text Filter}} rather than {{Click Text}} directly, so PII never leaves the browser in the hit payload.

Internal or External Link

GTM's built-in Auto-Event Variable can extract the IS_OUTBOUND component of a click URL, but it returns a boolean — true or false — which is awkward to filter in GA4. The Internal or External Link variable is a simple Lookup Table that converts false → "internal_link" and true → "external_link", producing a readable string dimension in GA4 reports.

Click URL Decomposition

Several URL-component variables are included so you can segment clicks by destination without parsing URLs in BigQuery:

VariableTypeReturns
Click URL Host NameAuto-Event Variable (HOST, lowercase, strip www)The hostname of the clicked URL, e.g. example.com
Click URL PathAuto-Event Variable (PATH, lowercase)The full path of the clicked URL
Click URL ExtensionAuto-Event Variable (EXTENSION, lowercase)File extension of the clicked URL, if any (e.g. pdf, docx)
Click URL DocumentPage Path Part N template (last segment, lowercase)The last path segment of the clicked URL — typically the filename or page slug

Environment-Aware Stream ID Routing

The container ships with a two-tier routing chain that keeps development traffic off the production GA4 property:

  1. Measurement Stream RegEx Lookup checks the {{Page URL}} against a list of non-production hostname patterns (dev|uat|qa|orig|stg|stage|staging|int|local|admin|aem as a URL prefix pattern, plus the GTM Preview appspot.com domain). If matched, it routes to {{Measurement Stream ID Development}}; otherwise it falls through to the next variable.
  2. Environment Stream ID checks the GTM {{Environment Name}} built-in (populated when using GTM Environments). Live/production environments match ^liv.*; staging and pre-production environments match ^(pre|de|st|ua|qa|te).*. The default is the production stream ID.
  3. Both Measurement Stream ID Production and Measurement Stream ID Development are simple Constant variables you update to your actual GA4 Measurement IDs.

GA4 Event Parameters

The All Clicks Send Event tag sends a GA4 event named click with these parameters:

GA4 ParameterGTM VariableDescription
click_id{{coalesce id}}First non-empty id found on the clicked element or its ancestors (up to 10 levels)
click_class{{coalesce className}}First non-empty className found on the clicked element or its ancestors
click_text{{Click Text Filter}}Visible text of the clicked element with phone numbers and email addresses masked
click_url{{Click URL}}Full URL of the clicked link (empty for non-link elements)
click_element{{Click Element}}The DOM element reference (useful in GTM Preview; not a string dimension in GA4)
click_type{{Internal or External Link}}internal_link or external_link
click_target{{Click Target}}The target attribute of the clicked link (_blank, etc.)
click_url_document{{Click URL Document}}Last path segment of the clicked URL (filename or slug)
click_url_extension{{Click URL Extension}}File extension of the clicked URL
click_url_hostname{{Click URL Host Name}}Hostname of the clicked URL
click_url_path{{Click URL Path}}Full path of the clicked URL
page_location{{Page URL}}Full URL of the page where the click occurred
page_path{{Page Path}}Path of the page where the click occurred
page_hostname{{Page Hostname}}Hostname of the page where the click occurred
page_referrer{{Referrer}}HTTP referrer of the page
click_alt{{Click Alt Text}}The alt attribute of the clicked element, lowercased (useful for image clicks)
click_data_testid{{coalesce data-testid}}First non-empty data-testid found on the clicked element or its ancestors
click_aria_label{{coalesce aria-label}}First non-empty aria-label found on the clicked element or its ancestors
GA4 custom dimensions: To use click_data_testid, click_aria_label, click_type, and the URL decomposition parameters in GA4 reports and Explorations, register them as custom event-scoped dimensions in your GA4 property under Admin → Custom definitions → Custom dimensions.

Instrumenting Your HTML: The Developer Contract

The coalesce variables give you a clean channel to communicate with your development team. Instead of asking them to push custom dataLayer events for every interactive component, agree on two attributes:

<!-- Navigation card component root -->
<div
  data-testid="nav-card-products"
  aria-label="Products navigation card"
>
  <!-- Inner icon button that actually receives the click -->
  <button>
    <svg><use href="#icon-arrow"/></svg>
  </button>
</div>

When the user clicks the SVG, GTM's {{Click ID}} and {{Click Classes}} will be empty or uninformative. But {{coalesce data-testid}} will walk up to the <div> and return nav-card-products, and {{coalesce aria-label}} will return Products navigation card — both without any JavaScript, dataLayer push, or CSP change.

How to Import

  1. Download the JSON file from the Google Drive link.
  2. In GTM, go to Admin → Import Container.
  3. Upload universal-click-tracking-recipe.json.
  4. Choose Merge (not Overwrite) to preserve your existing tags and triggers.
  5. Update Measurement Stream ID Production and Measurement Stream ID Development constant variables to your actual GA4 Measurement IDs.
  6. Review the Measurement Stream RegEx Lookup variable and add or remove hostname patterns that match your non-production environments.
  7. In GA4, register click_data_testid, click_aria_label, click_type, click_url_document, click_url_extension, click_url_hostname, and click_url_path as custom event-scoped dimensions.
  8. Open GTM Preview mode, interact with your site, and verify the click event parameters in the Tag Assistant panel before publishing.
Performance note: The trigger fires on every element click with no filter. If your site has very high click volumes and you are on a GA4 free property (which has a 1,000 events-per-session limit before sampling), consider scoping the trigger to specific elements or adding a trigger exception for clicks on non-interactive elements. On most informational and e-commerce sites the raw click volume stays well within free-tier limits.

What You Can Do With It

  • Build GA4 Funnel Exploration reports using click_data_testid values that match your design system component names
  • Identify which icon buttons and image links are clicked most often using click_aria_label, even when those elements have no visible text
  • Segment outbound link clicks from internal navigation in a single dimension using click_type
  • Track PDF and document downloads automatically via click_url_extension = "pdf" without a separate File Download trigger
  • Use click_url_hostname to identify clicks to third-party partner or vendor sites
  • Combine click_data_testid with your QA test suite to confirm that instrumented components are also being tracked correctly in analytics

The full source — including the container JSON, variable inventory, and the Sublimetrix community templates — is available on GitHub. If you adapt the coalesce depth or add new attribute families, open a pull request or issue.

⬇ Download GTM Container JSON

Comments

Popular posts from this blog

Site Landing & Site Referrer Preservation

GTM Browser Viewport Measurement

UTM & URL Query String 2 Cookies