Most "conversion rate" numbers on dashboards are wrong in one boring way: the denominator. People divide signups by all sessions, by pageviews, or by "visitors" from one tool and conversions from another. Then they wonder why the rate jumps around.
This post covers how to define the funnel, instrument it with plain JavaScript, and compute a rate you can trust.
1. Pick one goal and one denominator
Conversion rate = conversions ÷ visitors × 100.
The formula is easy. The hard part is deciding:
- What counts as a conversion? Pick one: a submitted form, a completed checkout, a booked call. Don't blend them.
- Who counts as a visitor? Unique visitors to the page that holds the goal is usually the honest denominator. Sitewide sessions inflate it.
Example: 10,000 visitors to a landing page and 250 signups gives 2.5%.
2. Split the form funnel into three events
A single "submit" event hides where people drop. Track three:
-
form_view: the form actually entered the viewport (not just page load). -
form_start: first interaction with any field. -
form_submit: a successful submit (after validation, not on click).
Zuko's 2025 benchmarks put view-to-submit abandonment around 55%, and contact forms convert about 9% of the people who view them. Without the view/start split you can't tell a "nobody sees it" problem from a "people start and quit" problem.
3. Instrument it
// Minimal form funnel tracking. Swap `track` for your analytics call.
function track(event, props = {}) {
// e.g. gtag('event', event, props) or posthog.capture(event, props)
console.log(event, props);
}
function instrumentForm(form, name) {
let viewed = false, started = false;
new IntersectionObserver((entries, obs) => {
if (entries.some(e => e.isIntersecting) && !viewed) {
viewed = true;
track('form_view', { form: name });
obs.disconnect();
}
}, { threshold: 0.5 }).observe(form);
form.addEventListener('focusin', () => {
if (!started) {
started = true;
track('form_start', { form: name });
}
});
form.addEventListener('submit', (e) => {
if (form.checkValidity()) track('form_submit', { form: name });
});
}
document.querySelectorAll('form[data-track]').forEach(f =>
instrumentForm(f, f.dataset.track)
);
Mark forms with <form data-track="contact">. If you submit via fetch, fire form_submit in the success handler instead, so failed requests don't count.
4. Compute three rates, not one
- Visibility: form_view ÷ page visitors. If it's low, move the form up or make it reachable.
- Start rate: form_start ÷ form_view. If it's low, the offer or the first field is the problem.
- Completion: form_submit ÷ form_start. If it's low, look at length, unclear fields or trust.
- Overall conversion: form_submit ÷ page visitors.
5. Sanity-check the math and size the upside
Before arguing about a redesign, plug your visitors and conversions into a calculator to check the rate. Then see what a 10–25% lift would be worth. We built a free one with a what-if slider and no sign-up: conversion rate calculator. A spreadsheet works too. The point is to put a number on the upside before spending a sprint on it.
6. Common gotchas
- Bots and your own team inflate visitors, so filter internal IPs.
- Single-page apps: fire page-visitor events on route change, not just on load.
- Compare like periods (weekday vs weekday). Small sites need weeks of data before a change in rate means anything.
- Change one thing at a time so you know what moved the rate.
Wrap-up
Define one goal, use the page's own visitors as the denominator, and split the form into view → start → submit. You'll know which step leaks, and that tells you what to fix.
The EasyFunnel team. We build always-on lead capture for websites.
Top comments (1)
I like the distinction between views, starts, and submits. Tracking these as separate events makes it much easier to identify where users drop off instead of relying on a single conversion number. How do you handle duplicate submissions when users refresh the page or submit a form more than once?