/>
Forms & Funnels
Back to portfolio Funnels

Multi-Step Service Request Funnel

A branching, auto-advancing service-request form for a fictional handyman company — with a built-in A/B test, Consent Mode–aware GTM tracking, and a real webhook handoff.

HTML CSS Vanilla JavaScript Google Tag Manager GA4 Consent Mode
Fix It Fellas Project
GTM Preview Mode Example
GA4 Events Example
Role Solo build
Status Live demo
Type Service-request funnel

A home-services request form needs to move fast — most visitors already know what they need, so every unnecessary click or "Continue" button in the way costs completions. At the same time, the form still has to collect real contact information safely, respect a visitor's cookie choice before any tracking or ad signals fire, and hand off a completed lead somewhere useful. This project needed all three working together in one lightweight, no-framework build.

  • A four-step request flow (service category → specific service, job details, contact info, preferred schedule), where most steps auto-advance the instant a valid choice is made — no separate "Next" click required except on the typed-input and final steps.
  • Full field-level validation on the contact step — required-field checks plus regex validation for email and phone, with a live-formatting input mask on the phone field, plus a real required consent checkbox rather than a paragraph of fine print.
  • A built-in A/B test on the form itself: each visitor is randomly assigned one of two visual variants (navy background with white icons, or white background with blue icons) on first load, persisted via localStorage so it stays consistent across their visit.
  • dataLayer.push() events at every step (step_1_complete through step_3_complete, plus form_submitted), every single one automatically carrying the visitor's assigned ab_variant so results can be segmented by variant in GA4 with no extra setup.
  • Real Google Consent Mode: analytics_storage, ad_storage, and related signals default to denied before GTM even loads, and only update to granted or denied once the visitor makes a choice on the cookie banner — with a returning visitor's earlier choice automatically re-applied on their next visit.
  • A working fetch() call posting the completed request to a webhook endpoint, proving the client-side handoff to a CRM.
Auto-advance, but keep Back everywhere.

Auto-advance on a single click." Most steps only need one choice to complete, so the form advances automatically the instant a valid selection is made rather than waiting on a separate click. The Next/Back buttons stay visible on every step regardless, both for a consistent layout across all four steps and as a fallback — attempting to advance without a valid selection still surfaces the existing validation error.

A/B variant folded into every existing event, not tracked separately.

Rather than building a standalone A/B tracking system, the assigned variant rides along as one extra parameter on the funnel events already being built. One small helper function guarantees every event carries it automatically, so it's never possible to forget it on one push and lose that data point.

Consent Mode over a simple "don't track if declined" check.

Rather than wrapping tracking calls in an if-statement, this uses Google's actual Consent Mode API — a default-denied state set before GTM loads, updated once the visitor chooses. That's the same mechanism a real consent-management plugin (CookieYes, at my day job) implements under the hood, so building it by hand here was about understanding what that kind of plugin is actually doing, not just what a cookie banner looks like.

The webhook succeeded — but the response didn't.

Unlike Project 1's CRM endpoint, this webhook test target accepted the request outright — the submitted data genuinely arrived. But since the endpoint's response doesn't include the right CORS headers, the browser still blocks JavaScript from reading that response, surfacing as a failed fetch() in the console even though delivery succeeded. It's a useful, accurate example of why "my fetch failed" doesn't always mean "the data never arrived" — and exactly the kind of gap a server-side proxy. I will be learning this in my next phase of learning.