Most agency contact forms have one box labelled "Message". People write "Hi, I'd like some information" and you learn nothing. So you add fields — budget, timeline, project type — and now the form has nine questions and nobody finishes it.
The mistake is treating every visitor the same.
Someone who wants a written quote will answer five questions, because they get a number in return. It's a fair trade and they know it. Someone who just wants to talk to a human will not answer any of them — asking a stranger for their budget before they've heard your voice is the fastest way to lose them.
So ask one question first:
Do you want a quote, or do you want to talk?
Then send them down different paths. Five questions for the first. Two for the second. Same form, two lengths, and neither group is treated like the other one.
const config = {
entryStep: "intent",
branches: [
{ when: "quote", steps: ["intent", "scope", "budget", "timing", "contact"] },
{ when: "talk", steps: ["intent", "contact"] },
],
// …
};
Four things I got wrong first
"I don't know yet" has to be a valid answer.
I left it out of the budget step, thinking it would push people to commit to a number. It did the opposite: they either guessed something misleading, or closed the tab. An honest "no idea yet" tells you more than a made-up 5.000.
Error messages should say how to fix it.
"This field is required" makes people hunt for what's missing. "That address is missing the @ or the domain" does not. It costs one sentence per rule and it's the difference between a form people finish and one they abandon.
Save the draft.
Someone fills half the form, gets interrupted, comes back to an empty page — and does not start again. localStorage, wrapped in try/catch so a private window degrades to no draft rather than a crash. Clear it on submit.
The progress bar lies before the first answer.
This one took me longest to see. With branching, you don't know how many steps there are until they pick a path. My first version showed "Step 1 of 1" and a Send button on the opening question, because the branch wasn't decided yet. Show the longest branch as the estimate instead: better to promise six and ask three than the reverse.
On the bot field
An off-screen input no human fills. If it arrives with a value, drop the submission — and tell the bot it worked. A bot told it failed just tries again.
Position it off-screen rather than display: none: some bots skip hidden fields but happily fill a positioned one.
.qf-trap {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
It is not a captcha and will not stop a determined attacker. It stops the cheap ones, which is most of them, without making a single real person solve a puzzle.
I packaged all of this as a React component — no dependencies, every string configurable, light and dark mode, around 530 commented lines. It's called QuoteFlow — that page has a live demo you can click through, branching and all — if it saves you the afternoon.
Top comments (0)