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.
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.
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
| Folder | Contains |
|---|---|
| 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
| Tag | Type | Purpose |
|---|---|---|
Google Analytics Configuration | Google 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 Event | GA4 Event (gaawe) | Fires a GA4 click event on every click, forwarding the full set of enriched click parameters. |
Trigger
| Trigger | Type | Purpose |
|---|---|---|
All Clicks All Elements | Click — All Elements | Fires on every click on the page with no additional filter conditions. The enrichment happens in the variables, not the trigger. |
Custom Templates (from Sublimetrix)
| Template | Purpose |
|---|---|
If Else If — Advanced Lookup Table | Powers 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 N | Decomposes 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.
<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:
- User clicks anything on the page. The All Clicks All Elements trigger fires immediately — it has no filter conditions.
- 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.elementobject. - DOM traversal variables are evaluated. For each attribute family (
id,className,aria-label,data-testid), a ladder of Data Layer Variables reads the value atgtm.element, then.parentElement, then.parentElement.parentElement, and so on up to ten ancestors. - 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}}. - 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 stringsinternal_linkorexternal_link. {{Click URL Document}} extracts the last path segment of the clicked URL using the Page Path Part N template. - The GA4 event fires. The All Clicks Send Event tag sends a
clickevent 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 Name | Data Layer Path |
|---|---|
DOM data-testid | gtm.element.attributes.data-testid.value |
DOM parent data-testid | gtm.element.parentElement.attributes.data-testid.value |
DOM grandparent data-testid | gtm.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).
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
' *** ':
| Pattern | Matches |
|---|---|
\([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:
| Variable | Type | Returns |
|---|---|---|
Click URL Host Name | Auto-Event Variable (HOST, lowercase, strip www) | The hostname of the clicked URL, e.g. example.com |
Click URL Path | Auto-Event Variable (PATH, lowercase) | The full path of the clicked URL |
Click URL Extension | Auto-Event Variable (EXTENSION, lowercase) | File extension of the clicked URL, if any (e.g. pdf, docx) |
Click URL Document | Page 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:
- 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|aemas 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. - 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. - Both
Measurement Stream ID ProductionandMeasurement Stream ID Developmentare 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 Parameter | GTM Variable | Description |
|---|---|---|
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 |
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
- Download the JSON file from the Google Drive link.
- In GTM, go to Admin → Import Container.
- Upload
universal-click-tracking-recipe.json. - Choose Merge (not Overwrite) to preserve your existing tags and triggers.
- Update
Measurement Stream ID ProductionandMeasurement Stream ID Developmentconstant variables to your actual GA4 Measurement IDs. - Review the
Measurement Stream RegEx Lookupvariable and add or remove hostname patterns that match your non-production environments. - In GA4, register
click_data_testid,click_aria_label,click_type,click_url_document,click_url_extension,click_url_hostname, andclick_url_pathas custom event-scoped dimensions. - Open GTM Preview mode, interact with your site, and verify the
clickevent parameters in the Tag Assistant panel before publishing.
What You Can Do With It
- Build GA4 Funnel Exploration reports using
click_data_testidvalues 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_hostnameto identify clicks to third-party partner or vendor sites - Combine
click_data_testidwith 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
Post a Comment