GTM Form Tracking: Complete Setup Guide (All Form Types)

Learn how to track form submissions in Google Tag Manager. Covers standard forms, AJAX forms, multi-step forms, third-party embeds, and form abandonment.

GTMform trackingconversion trackinglead generationGoogle Tag Manager

Form submissions are the lifeblood of lead generation. But tracking them in Google Tag Manager can be surprisingly complex. Standard HTML forms work differently than AJAX forms, which work differently than embedded third-party forms. Here’s how to track them all.

Understanding Form Submission Types

Before setting up tracking, identify what type of form you have:

1. Standard HTML Forms

Traditional forms that submit to a server and load a new page.

<form action="/submit" method="POST">
  <input name="email" type="email">
  <button type="submit">Submit</button>
</form>

Tracking method: GTM Form Submission trigger

2. AJAX Forms

Forms that submit without page reload (React, Vue, fetch API).

form.addEventListener('submit', async (e) => {
  e.preventDefault();
  await fetch('/api/submit', { method: 'POST', body: formData });
  showThankYou();
});

Tracking method: Custom Event trigger or Element Click trigger

3. Third-Party Embedded Forms

Forms from HubSpot, Typeform, Gravity Forms, etc., often in iframes.

<iframe src="https://forms.hubspot.com/..."></iframe>

Tracking method: postMessage listener or platform-specific callback

4. Multi-Step Forms

Wizards with multiple pages/steps before final submission.

Tracking method: Custom events for each step

Method 1: Standard HTML Form Tracking

Step 1: Enable Form Variables

  1. GTM → Variables → Built-In Variables → Configure
  2. Enable all Form-related variables:
    • Form Element
    • Form Classes
    • Form ID
    • Form Target
    • Form URL
    • Form Text

Step 2: Create Form Submission Trigger

  1. Triggers → New
  2. Trigger Type: Form Submission
  3. Configure:
    • This trigger fires on: Some Forms
    • Condition: Form ID equals contact-form (or your form’s ID)

Alternative conditions:

Form Classes contains "contact-form"
Form URL contains "/contact"
Page Path equals "/contact"

Step 3: Create GA4 Event Tag

  1. Tags → New
  2. Tag Type: Google Analytics: GA4 Event
  3. Event Name: form_submit or generate_lead
  4. Parameters:
    • form_id: {{Form ID}}
    • form_destination: {{Form URL}}
    • page_path: {{Page Path}}
  5. Trigger: Your Form Submission trigger

Step 4: Handle “Check Validation” Option

The “Check Validation” option prevents the trigger from firing if form validation fails:

  • Checked (recommended): Only fires on successful submission
  • Unchecked: Fires on any submit click, even if form has errors

Gotcha: Check Validation only works with standard HTML5 validation. Custom JavaScript validation isn’t detected.

Method 2: AJAX Form Tracking

AJAX forms don’t trigger GTM’s Form Submission trigger. You have three options:

Modify your form’s success handler:

// Your existing AJAX form handler
async function handleSubmit(e) {
  e.preventDefault();

  try {
    const response = await fetch('/api/submit', {
      method: 'POST',
      body: new FormData(e.target)
    });

    if (response.ok) {
      // Push success event to data layer
      dataLayer.push({
        'event': 'form_submission',
        'form_id': e.target.id,
        'form_name': 'contact_form'
      });

      showThankYou();
    }
  } catch (error) {
    showError();
  }
}

GTM Trigger:

  1. Trigger Type: Custom Event
  2. Event name: form_submission

Option B: Track Success Element Visibility

If you can’t modify the form code, track when the success message appears:

// Create an Element Visibility trigger
// Element selector: .form-success-message or #thank-you
// When to fire: Once per page

GTM Trigger:

  1. Trigger Type: Element Visibility
  2. Selection Method: CSS Selector
  3. Element Selector: .success-message
  4. Fire on: Once per page

Option C: Intercept Fetch/XMLHttpRequest

Advanced: Listen for specific API calls:

// Add to a Custom HTML tag that fires on All Pages
(function() {
  const originalFetch = window.fetch;

  window.fetch = function(...args) {
    return originalFetch.apply(this, args).then(response => {
      // Check if this is a form submission endpoint
      if (args[0].includes('/api/contact-submit') && response.ok) {
        dataLayer.push({
          'event': 'ajax_form_submit',
          'form_endpoint': args[0]
        });
      }
      return response;
    });
  };
})();

Method 3: Third-Party Form Tracking

HubSpot Forms

HubSpot provides callbacks. Add this Custom HTML tag:

<script>
window.addEventListener('message', function(event) {
  if (event.data.type === 'hsFormCallback' && event.data.eventName === 'onFormSubmitted') {
    dataLayer.push({
      'event': 'hubspot_form_submit',
      'form_id': event.data.id,
      'form_name': event.data.data.formGuid
    });
  }
});
</script>

Or use HubSpot’s official callback:

hbspt.forms.create({
  portalId: "YOUR_PORTAL_ID",
  formId: "YOUR_FORM_ID",
  onFormSubmit: function($form) {
    dataLayer.push({
      'event': 'hubspot_form_submit',
      'form_id': 'YOUR_FORM_ID'
    });
  }
});

Typeform

Typeform uses postMessage:

window.addEventListener('message', function(event) {
  if (event.origin === 'https://form.typeform.com') {
    if (event.data.type === 'form-submit') {
      dataLayer.push({
        'event': 'typeform_submit',
        'form_id': event.data.formId
      });
    }
  }
});

Gravity Forms (WordPress)

Gravity Forms fires JavaScript events:

jQuery(document).on('gform_confirmation_loaded', function(event, formId) {
  dataLayer.push({
    'event': 'gravity_form_submit',
    'form_id': formId
  });
});

Calendly

Calendly widget events:

window.addEventListener('message', function(event) {
  if (event.origin === 'https://calendly.com') {
    if (event.data.event === 'calendly.event_scheduled') {
      dataLayer.push({
        'event': 'calendly_booking',
        'event_type': event.data.payload.event_type.name,
        'invitee_email': event.data.payload.invitee.email
      });
    }
  }
});

Generic iframe Form Tracking

For iframes without callbacks, track the thank-you URL if the iframe navigates:

// Limited option: Track when iframe src changes
const iframe = document.querySelector('iframe.form-embed');
const observer = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    if (mutation.attributeName === 'src' &&
        iframe.src.includes('thank-you')) {
      dataLayer.push({ 'event': 'iframe_form_success' });
    }
  });
});

observer.observe(iframe, { attributes: true });

Note: Cross-origin iframes have severe limitations. If you can’t use postMessage callbacks, consider asking the form provider for tracking options.

Method 4: Multi-Step Form Tracking

Track each step of a multi-step form/wizard:

// Step progression tracking
function trackFormStep(stepNumber, stepName) {
  dataLayer.push({
    'event': 'form_step',
    'step_number': stepNumber,
    'step_name': stepName,
    'form_name': 'application_form'
  });
}

// On each step change:
document.querySelector('.next-step').addEventListener('click', function() {
  const currentStep = getCurrentStep(); // Your function
  trackFormStep(currentStep, getStepName(currentStep));
});

// On final submission:
dataLayer.push({
  'event': 'form_complete',
  'form_name': 'application_form',
  'total_steps': 5
});

Building a Funnel Report

In GA4:

  1. Explore → Create new exploration
  2. Choose “Funnel exploration” template
  3. Add steps: form_step with step_number = 1, 2, 3, etc.
  4. Analyze drop-off between steps

Method 5: Form Abandonment Tracking

Track when users start but don’t finish forms:

// Track form interaction start
let formStarted = false;
const form = document.getElementById('contact-form');

form.querySelectorAll('input, textarea').forEach(function(field) {
  field.addEventListener('focus', function() {
    if (!formStarted) {
      formStarted = true;
      dataLayer.push({
        'event': 'form_start',
        'form_id': form.id
      });
    }
  });
});

// Track abandonment on page unload
window.addEventListener('beforeunload', function() {
  if (formStarted && !formSubmitted) {
    // Note: beforeunload is unreliable, use navigator.sendBeacon
    navigator.sendBeacon('/api/analytics', JSON.stringify({
      event: 'form_abandon',
      form_id: form.id
    }));
  }
});

Better Abandonment Tracking with Visibility API

document.addEventListener('visibilitychange', function() {
  if (document.visibilityState === 'hidden' && formStarted && !formSubmitted) {
    navigator.sendBeacon(
      'https://www.google-analytics.com/g/collect?v=2&tid=G-XXXXXXX&en=form_abandon',
      ''
    );
  }
});

Debugging Form Tracking

Step 1: Check Variable Values

In GTM Preview mode, click on any Form Submission event:

  • Variables tab shows all form variable values
  • Verify Form ID, Form Classes match your conditions

Step 2: Verify Trigger Conditions

If trigger isn’t firing:

  1. Check “Check Validation” isn’t blocking valid submissions
  2. Verify conditions match exactly (case-sensitive!)
  3. Try “All Forms” first to confirm form submission is detected

Step 3: Test AJAX Forms

For AJAX forms:

// Manually check data layer after submission
console.log(dataLayer.filter(d => d.event && d.event.includes('form')));

Step 4: Check for JavaScript Errors

Form tracking can fail silently if JavaScript errors occur:

  1. Open Console before submitting form
  2. Submit form
  3. Check for errors before your data layer push

Common Form Tracking Issues

Issue: Trigger Fires Before Validation

Symptom: Event fires even when form has errors.

Fix: Enable “Check Validation” in trigger, or track success message visibility instead.

Issue: Form ID is Empty

Symptom: Form ID variable shows undefined.

Fix: Your form doesn’t have an ID attribute. Use Form Classes or Page Path instead.

Issue: AJAX Form Not Detected

Symptom: No Form Submission event in Preview mode.

Fix: AJAX forms require custom event triggers. Add data layer push to success handler.

Issue: Form Submits Twice

Symptom: Two form_submit events for one submission.

Fix: Check for duplicate GTM containers or duplicate triggers:

// Add deduplication
let submitted = false;
form.addEventListener('submit', function() {
  if (submitted) return;
  submitted = true;
  // Your tracking code
});

Issue: Can’t Track iframe Form

Symptom: No access to form inside iframe.

Fix: Use postMessage callbacks from the form provider, or track the redirect URL after submission.

Complete Form Tracking Setup Checklist

  • Identify form type (standard, AJAX, third-party, multi-step)
  • Enable all Form built-in variables
  • Create appropriate trigger for form type
  • Add parameters (form_id, form_name, page_path)
  • Test in Preview mode
  • Verify in GA4 DebugView
  • Handle edge cases (validation, duplicates)
  • Set up conversion goal if needed
  • Test on mobile devices
  • Document form tracking for team

Sample GTM Container Configuration

Here’s a complete setup for tracking multiple form types:

Variables:

  • dlv_form_id - Data Layer Variable: form_id
  • dlv_form_name - Data Layer Variable: form_name

Triggers:

  1. Standard Form Submission

    • Type: Form Submission
    • Fire on: All Forms (or specific forms)
    • Check Validation: Enabled
  2. AJAX Form Success

    • Type: Custom Event
    • Event name: form_submission
  3. HubSpot Form Submit

    • Type: Custom Event
    • Event name: hubspot_form_submit

Tags:

GA4 Form Submit Event

  • Type: GA4 Event
  • Event name: generate_lead
  • Parameters:
    • form_id: {{dlv_form_id}}
    • form_name: {{dlv_form_name}}
  • Trigger: All three form triggers above

Need Help With Form Tracking?

Form tracking edge cases are endless—AJAX variations, custom validation, third-party quirks, SPAs, and more. If your forms aren’t tracking correctly:

Get a free form tracking audit and we’ll analyze your specific forms, identify tracking gaps, and ensure every lead is captured.