Track WordPress Form Submissions and Scroll Depth with Google Analytics 4

Google Analytics 4 replaced Universal Analytics in July 2023, but teams running GA4 alongside WordPress need to track not just page views but meaningful user events such as form submissions, button clicks, file downloads, and scroll depth — none of which GA4 captures automatically without additional configuration. The gtag.js snippet initialises the GA4 measurement protocol client and provides the gtag('event', ...) function for sending custom events with arbitrary parameters. Loading gtag.js asynchronously prevents it from blocking the main thread, but it must appear early in the <head> to start collecting data before users interact with the page. Injecting the snippet via a WordPress wp_head hook rather than editing header.php directly keeps the implementation update-safe and easy to remove. Scroll depth tracking using the Intersection Observer API fires a GA4 event at 25, 50, 75, and 100 percent scroll milestones — giving content teams data on how far readers consume long-form articles. Form submission tracking hooks into the submit event on forms that lack server-side success/failure reporting visible to GA4. Outbound link tracking fires a GA4 event when a click target URL starts with http and does not share the current hostname, revealing which external links drive the most exit traffic. The send_to parameter scopes events to a specific GA4 measurement ID, which is important on multi-brand sites running several GA4 properties simultaneously. The Open Graph post covers complementary social sharing metadata that influences click-through rates — the same pages tracked in GA4 benefit from well-formed OG tags to improve social traffic quality. The breadcrumb and structured data guide explains JSON-LD that search engines use — pairing it with GA4 event data gives a full picture of both crawl and user engagement. Verify events are firing correctly in GA4 DebugView (accessible via Admin → DebugView while the browser has the debug extension active) before publishing the tracking code to production.

Problem: WordPress sites using GA4 track only basic page views by default — form submissions, scroll depth, and outbound link clicks require custom event code that is absent from most theme and plugin implementations.

Solution: Inject the gtag.js snippet via wp_head, then add vanilla JavaScript event listeners for form submits, outbound links, and scroll milestones that call gtag('event', ...) with descriptive parameter objects.

// functions.php — inject GA4 snippet
add_action('wp_head', function() {
    $measurement_id = 'G-XXXXXXXXXX';
    ?>
<script async src="https://www.googletagmanager.com/gtag/js?id=<?= esc_attr($measurement_id) ?>"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '<?= esc_js($measurement_id) ?>', { send_page_view: true });
</script>
    <?php
}, 1);

// wp-content/themes/mytheme/js/analytics-events.js
(function() {
    var MID = document.querySelector('script[src*="gtag/js"]')
                 ?.src.match(/id=(G-[A-Z0-9]+)/)?.[1] || '';

    // Form submission tracking
    document.addEventListener('submit', function(e) {
        var form = e.target.closest('form');
        if (!form) return;
        gtag('event', 'form_submit', {
            send_to:   MID,
            form_id:   form.id || form.action,
            form_name: form.dataset.formName || 'unknown',
        });
    });

    // Outbound link tracking
    document.addEventListener('click', function(e) {
        var a = e.target.closest('a[href]');
        if (!a) return;
        var url = a.href;
        if (url.startsWith('http') && !url.includes(location.hostname)) {
            gtag('event', 'click', {
                send_to:      MID,
                event_category: 'outbound',
                event_label:  url,
            });
        }
    });

    // Scroll depth milestones
    var fired = {};
    window.addEventListener('scroll', function() {
        var pct = Math.round(
            (window.scrollY + window.innerHeight) / document.body.scrollHeight * 100
        );
        [25, 50, 75, 100].forEach(function(m) {
            if (pct >= m && !fired[m]) {
                fired[m] = true;
                gtag('event', 'scroll_depth', { send_to: MID, depth: m });
            }
        });
    }, { passive: true });
}());

NOTE: Replace G-XXXXXXXXXX with your actual GA4 Measurement ID from Analytics → Admin → Data Streams, and enqueue the analytics events script with wp_enqueue_script('analytics-events', ..., [], '1.0', true) using in_footer: true so it loads after the gtag snippet in the head.