Skip to content

DOM Element Variables

DOM Element Variables let you read values directly from the page’s HTML — extracting text, attribute values, or other properties from elements on the page. They are useful when you cannot push data to the dataLayer, such as on legacy sites or third-party pages where you do not control the code.

That said, DOM Element Variables should be your last resort. DOM scraping is fragile: CSS class names change, element structure changes, and a redesign can silently break your tracking without any error. The dataLayer is a stable contract. The DOM is not. Use DOM variables only when you have no other option.

GTM gives you two distinct variable types for reading from the DOM:

  1. DOM Element Variable — reads a specific element’s property, selected by CSS selector or element ID
  2. Auto-Event Variable — reads a property of whatever element triggered the current event (the clicked element, the submitted form, etc.)

These are different tools for different situations.

The DOM Element Variable selects an element on the page and reads one of its properties. It evaluates at the moment the tag or trigger that uses it is evaluated — not at page load.

  1. Create a new User-Defined Variable and select DOM Element as the type.

  2. Choose the Selection Method:

    • CSS Selector — uses document.querySelector() to find the element. Most flexible.
    • ID — uses document.getElementById(). Slightly faster, but limited to ID attributes.
  3. Enter the selector or ID. For CSS Selector, enter the selector string (e.g., [data-product-id], #cart-total, .product-title).

  4. Set the Attribute Name to read. Leave blank to get the element’s text content (innerText). Enter an attribute name like href, data-product-id, or value to read that specific attribute.

  5. Set a Default Value for when the element is not found or the attribute does not exist.

<!-- HTML on the page -->
<div class="product-card" data-product-id="SKU-12345" data-product-price="199.99">
<h2 class="product-title">Leather Jacket</h2>
<span id="current-price">$199.99</span>
</div>
ConfigurationReturns
CSS Selector: [data-product-id], Attribute: data-product-idSKU-12345
CSS Selector: [data-product-price], Attribute: data-product-price199.99
CSS Selector: .product-title, Attribute: (blank)Leather Jacket
Element ID: current-price, Attribute: (blank)$199.99

document.querySelector() only returns the first matching element. If multiple elements on the page match your selector, you get the first one. For product listing pages with multiple products, this means you get the first product’s data regardless of which one was interacted with. This is one reason DOM Element Variables often disappoint — they cannot track which item triggered an event.

For that use case, you need the Auto-Event Variable or a dataLayer push from a click handler.

The Auto-Event Variable reads properties of the element that triggered the current GTM event — the clicked element, the submitted form, the visible element, or the element being scrolled past. It is context-aware in a way the DOM Element Variable is not.

Create a new User-Defined Variable, select Auto-Event Variable, and choose the Variable Type:

Variable TypeReturnsCommon Use
ElementThe DOM element itselfPass to other custom JS variables for further reading
Element AttributeA named attribute valuedata-product-id, href, aria-label
Element ClassesSpace-separated class stringCSS-class-based conditions
Element IDThe id attributeElement identification
Element TargetThe target attributeNew-tab detection
Element TextThe innerText contentButton/link text
Element URLThe resolved hrefLink destination
History New URL FragmentHash after history changeSPA navigation
History Old URL FragmentHash before history changeSPA back-navigation

For Element Attribute, you specify the exact attribute name in the “Attribute Name” field.

Reading data attributes from clicked elements

Section titled “Reading data attributes from clicked elements”

This is the most common use case:

<button class="add-to-cart"
data-product-id="SKU-12345"
data-product-name="Leather Jacket"
data-product-price="199.99">
Add to Cart
</button>

Create an Auto-Event Variable with:

  • Variable Type: Element Attribute
  • Attribute Name: data-product-id

When this button is clicked and a Click trigger fires, this variable returns SKU-12345. Create separate variables for data-product-name and data-product-price similarly.

The CSS selector you choose determines how stable your tracking is. Here is a reliability ranking from most to least stable:

Most stable:

  • ID selectors: #checkout-form — fast and unambiguous, but IDs must be unique
  • Data attribute selectors: [data-product-id] — semantically meaningful, resistant to redesign
  • ARIA attributes: [aria-label="Add to cart"] — designed to be stable identifiers

Moderately stable:

  • Name attributes: [name="email"] — good for form fields
  • Type attributes: [type="submit"] — works for simple cases, fails when there are multiple submit buttons

Least stable:

  • Class selectors: .product-title — changes with CSS refactors
  • Positional: :nth-child(3) — breaks when elements are added or reordered
  • Text content: DOM scraping by visible text — changes with copy rewrites

The closest() pattern for hierarchical data

Section titled “The closest() pattern for hierarchical data”

A common scenario: a product card has product data on the outer container, but the user clicks a button nested inside. document.querySelector() finds elements from the top — it cannot know which button was clicked. Auto-Event Variable gives you the clicked element, but the data is on the parent.

The solution is a Custom JavaScript Variable that uses closest():

function() {
var clickedElement = {{Click Element}};
if (!clickedElement) return undefined;
var card = clickedElement.closest('[data-product-id]');
if (!card) return undefined;
return card.getAttribute('data-product-id');
}

closest() walks up the DOM tree from the clicked element, looking for an ancestor that matches the selector. This correctly returns the product ID for the card regardless of which child element inside the card was clicked.

DOM Element Variables run document.querySelector() every time they are evaluated — which happens on every event that uses them in a trigger condition or tag. On complex pages with many elements, document.querySelector('[data-product-id]') is fast. But deeply nested selectors or selectors that traverse large subtrees can add measurable overhead.

For Auto-Event Variables, the performance cost is negligible — GTM has already identified the element that triggered the event, so reading its attributes is a property access, not a DOM traversal.

<!-- Bad: class name changes frequently -->
<button class="btn-primary add-to-cart-v2">Add to Cart</button>
<!-- Selector: .add-to-cart-v2 — breaks on next redesign -->
<!-- Good: data attribute is stable -->
<button class="btn-primary" data-action="add-to-cart">Add to Cart</button>
<!-- Selector: [data-action="add-to-cart"] — survives redesigns -->

Forgetting that DOM Element Variables get the first match

Section titled “Forgetting that DOM Element Variables get the first match”

If you use a DOM Element Variable with selector .product-price on a page with 20 products, you always get the price of the first product on the page, not the product the user clicked. For interaction-specific data, use Auto-Event Variables or push to the dataLayer from your click handler.

When the element does not exist on the page (wrong page type, element not rendered yet, A/B test variant without the element), the variable returns undefined. Set a sensible default like (not set) to prevent undefined from reaching your tags.

<!-- Text changes break this -->
<button class="cta">Get Started for Free</button>
<!-- Using blank attribute to read inner text: returns "Get Started for Free" -->
<!-- Next sprint: "Start Free Trial" — your tracking silently breaks -->

Text content is the least stable thing you can read from the DOM. Use data-* attributes.

Despite their fragility, there are legitimate use cases:

  • Legacy systems: Older CMS or e-commerce platforms with no developer access
  • Third-party embeds: Widgets you cannot modify but need to extract data from
  • Quick wins: Temporary tracking where speed matters more than long-term stability
  • Verification: Quickly checking what value is on the page during debugging

For any of these: document clearly that you are using DOM scraping and why, so the next person who inherits the container knows to watch for breakage.