Skip to content

Track Element Visibility

Element visibility tracking answers “did the user actually see this?” rather than “did the page load?” It is the foundation of impression tracking, ad viewability measurement, and content performance analysis.

GTM’s Element Visibility Trigger (simplest approach)

Section titled “GTM’s Element Visibility Trigger (simplest approach)”

GTM has a built-in Element Visibility trigger that watches for DOM elements entering the viewport using IntersectionObserver internally.

  1. Mark your elements for tracking

    Add an id or unique CSS class to any element you want to track:

    <div id="pricing-section" class="track-visibility" data-content-name="Pricing CTA">
    <!-- Your content -->
    </div>
    <div id="testimonials" class="track-visibility" data-content-name="Testimonials">
    <!-- Your content -->
    </div>
  2. Create an Element Visibility Trigger

    • Trigger type: Element Visibility
    • Selection method: CSS Selector.track-visibility
    • When to fire this trigger: Once per element (prevents re-firing if element scrolls off and back)
    • Minimum Percent Visible: 50 (IAB standard for ad viewability is 50% for 1 second)
    • Observe DOM changes: checked (handles dynamically injected elements)
  3. Create variables to identify which element fired

    GTM provides built-in variables for Element Visibility:

    • Enable Visible Element: returns the DOM element itself

    Create a Custom JavaScript Variable VE - content_name:

    function() {
    var el = `{{Visible Element}}`;
    return el ? (el.dataset.contentName || el.id || el.className) : undefined;
    }
  4. Create a GA4 Event Tag

    • Tag type: Google Analytics: GA4 Event
    • Event name: element_visible
    • Parameters:
      • content_name{{VE - content_name}}
      • element_id{{Element Visibility ID}}
      • page_path{{Page Path}}
    • Trigger: the Element Visibility trigger
  5. Test in Preview Mode

    Visit the page and scroll down until the tracked element enters the viewport. The element_visible event should fire in the Summary pane when the element reaches 50% visibility.

Tag Configuration

GA4 - element_visible

Type
Google Analytics: GA4 Event
Trigger
Element Visibility - .track-visibility
Variables
VE - content_nameElement Visibility IDPage Path

Custom IntersectionObserver approach (more control)

Section titled “Custom IntersectionObserver approach (more control)”

For batch impression tracking (product grids, ad slots, article lists), the custom approach is more efficient than firing individual GTM events per element.

dataLayer.push() element_visible

Batch impression tracking with IntersectionObserver — tracks multiple elements efficiently.

(function() {
var impressionQueue = [];
var flushTimer = null;
function flushImpressions() {
if (impressionQueue.length === 0) return;
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'element_visible',
visible_elements: impressionQueue.slice(),
visible_count: impressionQueue.length,
page_path: window.location.pathname
});
impressionQueue = [];
flushTimer = null;
}
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (!entry.isIntersecting) return;
var el = entry.target;
var contentData = {
content_name: el.dataset.contentName || el.id,
content_type: el.dataset.contentType || 'unknown',
content_id: el.dataset.contentId || null,
position: el.dataset.position || null
};
impressionQueue.push(contentData);
// Stop observing this element (only fire once)
observer.unobserve(el);
// Debounce flush to batch nearby impressions
clearTimeout(flushTimer);
flushTimer = setTimeout(flushImpressions, 200);
});
}, {
threshold: 0.5 // 50% visible
});
// Observe all tracked elements
document.querySelectorAll('[data-content-name]').forEach(function(el) {
observer.observe(el);
});
// Flush remaining impressions on page exit
window.addEventListener('beforeunload', flushImpressions);
})();
  1. Add data-content-name attributes to elements you want to track:

    <article data-content-name="How to Track PDFs" data-content-type="blog_post" data-content-id="post-123" data-position="1">
  2. Add the IntersectionObserver code as a Custom HTML tag with a Window Loaded trigger

  3. Create a Custom Event Trigger: element_visible

  4. Create a Data Layer Variable: DLV - visible_elementsvisible_elements

  5. Create a GA4 Event Tag:

    • Event name: element_visible
    • Parameter: visible_elements{{DLV - visible_elements}}
    • Note: This sends an array of objects. In GA4 BigQuery export, you can unnest the array for per-element analysis.

For advertising and sponsored content, use the IAB MRC viewability standard:

  • At least 50% of the element’s pixels are visible
  • For at least 1 continuous second

Add a time threshold to the observer:

var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
// Start 1-second timer
var timer = setTimeout(function() {
// IAB viewable: 50% visible for 1 second
pushViewableImpression(entry.target);
observer.unobserve(entry.target);
}, 1000);
// Cancel if element leaves viewport before 1 second
entry.target._viewTimer = timer;
} else {
// Element left viewport — cancel timer
clearTimeout(entry.target._viewTimer);
}
});
}, { threshold: 0.5 });
  1. Open GTM Preview and visit the page
  2. Scroll down until the tracked element enters the viewport
  3. The element_visible event should fire in the Summary pane
  4. Scroll back up and down again — it should NOT fire a second time (once per element)
  5. Verify the content_name value is correct

Element Visibility trigger fires on page load for above-the-fold elements. Elements that are already visible when the page loads will trigger the visibility event immediately. This is correct behaviour — the element is visible. If you only want to track below-the-fold impressions, add a Scroll Depth Threshold condition to the trigger.

Dynamic content is not observed. IntersectionObserver observes elements that exist when observe() is called. If your product grid loads after the observer is set up, new elements will not be tracked. Enable Observe DOM changes in the GTM trigger, or use MutationObserver to watch for new elements and call observer.observe() on them.

beforeunload impressions may be lost. The beforeunload flush calls dataLayer.push(), but if the page is navigating away, the GA4 tag may not have time to send the hit. Use navigator.sendBeacon() for the beforeunload flush.