Skip to content

Track Webflow Form Submissions

Webflow’s native forms have three success modes — inline message (the default), redirect to a URL, and redirect with query parameters. Each mode needs a slightly different tracking strategy. This recipe uses a MutationObserver for the inline-message mode (the most common), and falls back to a thank-you page push for redirect modes.

Valid as of April 2026, Webflow hosted.

Webflow submits forms via AJAX to Webflow’s own endpoint. On success it toggles the .w-form-done element to visible and hides .w-form (the form body). There is no public JavaScript event — you have to observe the DOM.

For redirect-mode forms, Webflow navigates to a configurable URL after success, so the signal is “page loaded with that URL.”

ModeDetection strategy
Inline success message (default)MutationObserver on the form wrapper
Redirect to URLThank-you page dataLayer push
Redirect with custom queryThank-you page push + parse query params

Add this as a Project Settings → Custom Code → Footer Code snippet. It observes every Webflow form on the page and fires once when the success message becomes visible.

dataLayer.push() form_submission

MutationObserver watches for Webflow's success state to become visible.

<script>
(function () {
function observeForm(wrapper) {
var form = wrapper.querySelector('form');
var done = wrapper.querySelector('.w-form-done');
if (!form || !done) return;
var fired = false;
var observer = new MutationObserver(function () {
if (fired) return;
var visible = done.style.display === 'block'
|| window.getComputedStyle(done).display === 'block';
if (!visible) return;
fired = true;
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'form_submission',
form_vendor: 'webflow',
form_name: form.getAttribute('data-name') || form.getAttribute('name') || 'unnamed',
form_id: form.id || '',
form_destination: 'inline_message'
});
});
observer.observe(done, { attributes: true, attributeFilter: ['style'] });
}
function init() {
document.querySelectorAll('.w-form').forEach(observeForm);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
  1. Create a Custom Event trigger

    • Trigger type: Custom Event
    • Event name: form_submission
    • This fires: Some Custom EventsDLV - form_vendor equals webflow
  2. Create Data Layer Variables

    • DLV - form_vendorform_vendor
    • DLV - form_nameform_name
    • DLV - form_idform_id
    • DLV - form_destinationform_destination
  3. Create a GA4 Event Tag

    • Event name: generate_lead
    • Parameters:
      • form_vendor{{DLV - form_vendor}}
      • form_name{{DLV - form_name}}
      • form_id{{DLV - form_id}}
      • form_destination{{DLV - form_destination}}
    • Trigger: the Custom Event trigger above
  4. Test in Preview mode

    Submit a Webflow form and confirm the event fires with the right form_name.

Tag Configuration

GA4 - generate_lead (Webflow)

Type
Google Analytics: GA4 Event
Trigger
Custom Event - form_submission (webflow)
Variables
DLV - form_vendorDLV - form_nameDLV - form_idDLV - form_destination

Webflow assigns data-name="Email Form" by default, which gives every form on the site the same name. Fix this in the Designer: select the form wrapper → Element settings → set a unique Name like contact_form or newsletter_signup. The data-name attribute follows that value.

You can also set an HTML ID on the form element itself (Element settings → ID). Prefer setting both — data-name for human-readable identifiers and id for CSS/JS targeting.

Webflow CMS Collection pages often embed the same form template per item (for example a contact form on every product page). The form_name will be identical across all pages. To disambiguate, add the CMS item slug to the dataLayer:

window.dataLayer.push({
event: 'form_submission',
form_vendor: 'webflow',
form_name: 'product_enquiry',
form_id: form.id,
cms_slug: window.location.pathname.split('/').pop()
});

Register cms_slug as a custom dimension in GA4 to break down submissions per product.

  1. Open GTM Preview mode on a Webflow page with a form.
  2. Submit the form with valid data. Confirm form_submission appears in the Summary pane with the correct form_name.
  3. Verify Tags Fired lists the GA4 tag.
  4. In GA4 DebugView, confirm generate_lead arrives within 15 seconds.
  5. Test failure path: disconnect your network and submit. Webflow shows the .w-form-fail element instead. Confirm no form_submission event fires.
  6. Test redirect mode: change the form’s After-sending setting to Redirect, submit, and confirm the thank-you page push fires instead.
  7. For CMS-bound forms, submit from two different CMS item pages and confirm cms_slug differs between the two events.

Custom Code not loading on all pages. Project Settings → Custom Code → Footer applies site-wide, but Page-level Custom Code overrides it on that page. If your observer script is only in Project Settings, check that no page has page-level code overriding it.

Webflow’s jQuery is namespaced. Webflow vendors jQuery under Webflow.require internally. Do not rely on window.jQuery being present — use vanilla JS as shown above.

Form inside a tabs/accordion component. If the form wrapper is inside a Webflow Tabs component that lazy-renders tab panes, the .w-form may not exist at DOMContentLoaded. Re-run init() on Webflow’s tab-change event, or use a broader MutationObserver on document.body watching for added .w-form nodes.

Duplicate forms on one page. Each .w-form wrapper gets its own observer — no double-fires. But Webflow’s default data-name="Email Form" gives both the same name. Rename them in the Designer.

Form export (exported HTML/CSS). If you export a Webflow site and self-host, Webflow’s form AJAX endpoint no longer works — forms either submit to a custom backend or fail. The .w-form-done pattern still applies if your custom backend mimics Webflow’s success state, but often you will replace it with a first-party form handler.

Multi-step forms. Webflow does not have native multi-step forms. Solutions typically use a community library (Finsweet Attributes or custom JS). Track steps via a separate form_step event from the step-handler code — follow the multi-step pattern in the generic form submissions recipe.

reCAPTCHA blocking. If Webflow’s reCAPTCHA setting is on but the user fails the challenge, .w-form-fail appears — not .w-form-done. Your observer correctly ignores this.