All WordPress HTML Templates Forms & Webhooks AI & Tools
Forms & Webhooks

Form Analytics: Measuring Drop-off Field by Field

Form analytics done properly: track field level drop off with sendBeacon, read the patterns, and fix the inputs killing your conversion rate. Code included.

A real working moment illustrating the theme of an article about form analytics. Wide 16:9 banner, one strong focal point, magazine editorial quality, authentic and unstaged.

A client ran a four-step quote form for commercial HVAC installs. Google Analytics said the form page got decent traffic and converted at roughly 9%. Everyone agreed the form was “fine” because nobody could point to a specific problem. Then we added field-level instrumentation and found that 41% of everyone who abandoned did so on the same input: a phone number field with a strict validation pattern that rejected spaces.

Fixing that one field took twenty minutes. Finding it took the instrumentation, because page-level analytics cannot see inside a form. To a pageview funnel, a form is one atomic step: they submitted or they didn’t. Everything interesting happens in the gap.

Form analytics is the practice of closing that gap: recording which fields were touched, in what order, how long each one held focus, how many times the user went back to correct it, and crucially which field was last touched before they left. It’s cheap to build and it consistently finds problems that no amount of design review will.

Key Takeaways

  • Track four things per field and you have almost everything you need: focus count, total dwell time, input event count (a proxy for correction effort), and whether the field was the last one touched before abandonment.
  • Use pagehide plus navigator.sendBeacon(), not unload. The unload event is unreliable in Safari, blocks bfcache, and silently drops data on mobile when the OS kills the tab.
  • Never record field values. Record metadata only: filled or empty, valid or invalid, character length bucket if you must. This keeps you out of GDPR special category trouble and out of the “we logged a credit card number” incident report.
  • The highest-signal metric is not drop-off count, it’s drop-off count divided by fields reached. A field that 100% of people reach and 5% abandon on is a smaller problem than one that 200 people reach and 30 abandon on.
  • Re-entry rate (focus count greater than two) almost always means the field label or the error message is wrong, not that the field itself is unnecessary.

Why page level analytics lies about forms

GA4’s default formstart and formsubmit events are better than nothing, and nothing is roughly what they tell you. They fire on first interaction and on submit. The entire middle of the experience, the part where people quit, is invisible.

Worse, submit-based conversion rates hide compositional shifts. Add a field and conversion drops two points: you know the form got harder. You don’t know whether the new field is the problem or whether it pushed an existing weak field below the fold on mobile. Both scenarios are real, and we’ve seen them on the same site two weeks apart.

Session replay tools get recommended here and they’re the wrong first move. Replay is qualitative, expensive per session, and you have to watch a lot of video to see a pattern. Instrument first, get a ranked list of suspect fields, then watch ten replays of that specific field if you still don’t understand why. Order matters.

A real working moment that fits an article section about

The four metrics that carry the signal

After instrumenting a few dozen forms, the useful set has stayed small.

  • Reach rate. What percentage of form starters ever focused this field. Divides your funnel properly and exposes fields nobody scrolls to.
  • Last field before exit. The single most actionable number. Aggregate it and you get a ranked abandonment table.
  • Dwell time. Total ms focused. A text input with a median dwell of 25 seconds is asking a question people have to go and look up.
  • Re-entry and edit count. How many times focus returned, and how many input events fired. High edits plus a failed checkValidity() is a validation problem. High re-entry with a valid value is a comprehension problem.

Note what isn’t on that list: keystroke content, mouse heatmaps, scroll depth within the form. We’ve tried all three. None of them changed a decision.

Instrumenting it in about sixty lines

You don’t need a vendor for this. Drop a single script on the page, mark the form with a data attribute, and send one beacon when the user leaves or submits.

(function () {
  const form = document.querySelector('[data-analytics-form]');
  if (!form) return;

  const formId = form.dataset.analyticsForm;
  const fields = [...form.elements].filter(el =>
    el.name && !['hidden', 'submit', 'button'].includes(el.type)
  );

  const state = new Map();
  let formStart = null;
  let lastTouched = null;
  let submitted = false;

  fields.forEach((el, index) => {
    state.set(el.name, {
      i: index, type: el.type,
      focus: 0, edits: 0, ms: 0, filled: false, invalid: false
    });

    let enteredAt = 0;

    el.addEventListener('focus', () => {
      enteredAt = performance.now();
      if (!formStart) formStart = Date.now();
      state.get(el.name).focus++;
    });

    el.addEventListener('input', () => { state.get(el.name).edits++; });

    el.addEventListener('blur', () => {
      const m = state.get(el.name);
      m.ms += Math.round(performance.now() - enteredAt);
      // metadata only, the value itself never leaves the browser
      m.filled = el.value.trim().length > 0;
      m.invalid = !el.checkValidity();
      lastTouched = el.name;
    });
  });

  form.addEventListener('submit', () => { submitted = true; send('submit'); });

  // pagehide fires where unload does not, and does not break bfcache
  addEventListener('pagehide', () => { if (!submitted) send('abandon'); });

  function send(reason) {
    if (!formStart) return; // never interacted: not an abandonment
    const payload = {
      form: formId,
      reason,
      last_field: lastTouched,
      duration_ms: Date.now() - formStart,
      viewport: innerWidth < 768 ? 'mobile' : 'desktop',
      fields: Object.fromEntries(state)
    };
    navigator.sendBeacon(
      '/collect/form',
      new Blob([JSON.stringify(payload)], { type: 'application/json' })
    );
  }
})();

Two details that bite people. First, use performance.now() for durations and Date.now() for wall clock. Mixing them gives nonsense when the tab sleeps. Second, the if (!formStart) return; guard matters. Without it, every bot and every bounce counts as an abandonment and your drop-off table is garbage within a day.

If you’re on a static site

You still need somewhere to POST. A small serverless function writing to Postgres or SQLite is fine. If the form submission itself already goes through a hosted endpoint such as WebForms, keep the analytics beacon on a separate path so a failing analytics endpoint never takes the actual lead with it. Never couple them. The submit must work even if the beacon 500s.

Form conversion tracking in GA4 versus your own table

You can push this into GA4 and it works, with caveats. Custom events accept up to 25 parameters, custom dimensions have to be registered before they collect, and high-cardinality string values get bucketed into “(other)” once you pass the reporting limits. Sending one event per field will burn through your event budget fast.

The pattern that holds up: one form_abandon event with the summary, and only send per-field detail to your own store.

gtag('event', 'form_abandon', {
  form_id: 'quote',
  last_field: 'phone',        // register as a custom dimension first
  fields_filled: 4,
  fields_total: 9,
  duration_ms: 38210
});

With your own table, the queries you actually run are trivial:

SELECT last_field,
       COUNT(*)                                AS abandons,
       ROUND(AVG(durationms) / 1000.0, 1)     AS avgseconds
FROM form_events
WHERE form = 'quote'
  AND reason = 'abandon'
  AND created_at > now() - interval '30 days'
GROUP BY last_field
ORDER BY abandons DESC;

Run that weekly. If the top row doesn’t change for a month, your form is as good as it’s getting without a structural redesign.

Reading the patterns

Raw numbers mislead until you know what each shape means. These are the ones we see repeatedly.

High edits, high invalid, short dwell. Validation is rejecting legitimate input. Phone and postcode fields own this category. The fix is almost always to loosen the pattern and normalise server-side, not to write a better error message.

Long dwell, low edits, then exit. The user is thinking, or looking something up, and gave up. Typical of “company size”, “budget range”, “VAT number”. Either make it optional or give it sensible defaults.

Focus count of three or more with a valid final value. People keep coming back to check it. The label is ambiguous or the field sits next to one that looks similar. We once found a form where “Site address” and “Billing address” had a combined re-entry rate over 60% purely because they were visually identical blocks.

Reach rate falling off a cliff mid-form on mobile only. Something below that point isn’t reachable: a sticky footer covering the next input, a date picker that opens off screen, a keyboard that pushes the submit button out of view. Split every metric by viewport or you will miss this entirely.

Sudden spike in invalid on one field, one browser. Usually an autofill collision. Check your autocomplete attributes before blaming the user.

Privacy, consent and what not to log

Record metadata, never content. The temptation to log “just the first three characters so we can see what people type” ends badly the day someone pastes a password into the wrong box. Our rule on client work: the collector endpoint rejects any payload containing a key named value, as a belt-and-braces check against someone extending the script later.

Under GDPR, field-level interaction metadata with no identifier and no content is a reasonable legitimate interest argument. Attach it to a user ID or a cookie that also feeds advertising, though, and it becomes analytics cookies requiring consent. Keep the beacon behind your consent gate if your legal position requires it, and accept that you’ll be measuring a sample rather than a census. A sample is still enough to rank fields.

One more thing to filter: bots. Spam submissions distort dwell time downwards and fill every field instantly. If you’re already running a honeypot or timing check as part of your contact form spam defence, tag those sessions and exclude them from the analytics table rather than deleting them. The bot traffic itself is useful data.

What to actually do with a ranked drop-off list

The instinct after seeing form drop-off data is to delete fields. Sometimes that’s right. Often it isn’t, because the field is there for a reason the sales team cares about and removing it just moves the friction to a phone call.

Cheaper interventions, roughly in order of how often they’ve worked for us:

  1. Loosen validation and normalise on the server. Strip spaces from phone numbers yourself. Accept uppercase postcodes.
  2. Move the hard field later. Commitment increases as the form progresses. A field that kills people at position two often survives at position seven.
  3. Replace free text with a select. Long dwell plus low edits means the user doesn’t know what format you want.
  4. Fix the error experience. Inline errors announced to assistive tech, focus moved to the first invalid field, messages that say what to do rather than what went wrong. We wrote the detail up in form accessibility: labels, errors and focus management, and the same changes that help screen reader users cut sighted-user abandonment too.
  5. Make it optional and see what happens. Then measure the fill rate. If 70% still answer it voluntarily, you never needed it required.

Then re-measure. Two weeks minimum, longer for low-traffic B2B forms where you might only see 200 starts a month. Resist shipping three changes at once: you will have no idea which one did the work.

Where this advice doesn’t apply

Below roughly 150 form starts per month, field-level statistics are noise. The last-field table will be dominated by whoever happened to abandon this week. Low-volume forms are better served by five moderated usability sessions, which cost less than the engineering time and tell you why rather than where. Instrument anyway (data accumulates), but don’t make decisions on twelve abandonments.

Frequently Asked Questions

Does tracking blur and focus events hurt performance?

No, measurably so. The listeners fire on user interaction only, the work per event is a counter increment, and the single beacon at the end is a few hundred bytes. We’ve profiled this on mid-range Android and the per-event cost sits in the microseconds. The script itself is under 2KB minified, which is less than most cookie banners.

Why not just use Hotjar, Microsoft Clarity or a dedicated form analytics tool?

Use them if you want replay and heatmaps alongside the numbers, and Clarity is free. The trade-offs are an extra third-party script in your critical path, session sampling you don’t control, and field-level data that’s hard to join against your CRM. Rolling your own gives you a table you can query next to your actual leads, which is where the interesting questions live.

How do I handle multi-step forms?

Treat each step as a named group and send the beacon on step transition as well as on pagehide, so you keep data even if a later step crashes. Add a step field to the payload and your drop-off query groups by step then by field. Multi-step forms also give you a free sanity check: step completion rates should decline smoothly, and a sharp cliff between two steps usually means the transition itself is broken on some device.

Will sendBeacon data get lost?

Some of it, yes. Beacons queued during pagehide are best-effort, and aggressive battery saving on mobile will drop a small percentage. You’re measuring relative differences between fields, not absolute totals, so a uniform loss rate doesn’t change the ranking. What would break the analysis is non-uniform loss, which is why unload is a poor choice: it fails disproportionately on iOS and skews your mobile data specifically.

Can I do this on a WordPress site with a plugin-built form?

Usually yes, as long as the plugin renders a real <form> with named inputs, which most do. Add data-analytics-form via a small JS snippet that finds the form by its generated class, or filter the form markup in your theme. The exception is forms rendered inside an iframe (some hosted embeds), where you have no access to the events and need the vendor’s own reporting.

Pick your highest-value form, add the sixty lines, and leave it running for a month before you change anything. The ranked table you get back is almost never the field the team argued about in the design review. That gap between what you assumed and what the data says is the entire point, and it’s why form conversion tracking at the field level pays for itself on the first fix.

form abandonment rate per field form analytics form analytics field level tracking form conversion tracking GA4 custom events measure form drop off by field sendBeacon pagehide form abandonment tracking track form field dwell time javascript why users abandon web forms validation