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
- GTM → Variables → Built-In Variables → Configure
- Enable all Form-related variables:
- Form Element
- Form Classes
- Form ID
- Form Target
- Form URL
- Form Text
Step 2: Create Form Submission Trigger
- Triggers → New
- Trigger Type: Form Submission
- 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
- Tags → New
- Tag Type: Google Analytics: GA4 Event
- Event Name:
form_submitorgenerate_lead - Parameters:
form_id:{{Form ID}}form_destination:{{Form URL}}page_path:{{Page Path}}
- 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:
Option A: Push to Data Layer (Recommended)
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:
- Trigger Type: Custom Event
- 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:
- Trigger Type: Element Visibility
- Selection Method: CSS Selector
- Element Selector:
.success-message - 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:
- Explore → Create new exploration
- Choose “Funnel exploration” template
- Add steps:
form_stepwith step_number = 1, 2, 3, etc. - 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:
- Check “Check Validation” isn’t blocking valid submissions
- Verify conditions match exactly (case-sensitive!)
- 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:
- Open Console before submitting form
- Submit form
- 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_iddlv_form_name- Data Layer Variable:form_name
Triggers:
-
Standard Form Submission
- Type: Form Submission
- Fire on: All Forms (or specific forms)
- Check Validation: Enabled
-
AJAX Form Success
- Type: Custom Event
- Event name:
form_submission
-
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}}
- form_id:
- 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.