Navigation and Routing
How visitors move through an SDK form: markers, branching, disqualification, and submit semantics
SDK form records carry no routing logic. Branching lives in your own code. The SDK moves between steps and keeps the response in sync.
Declarative Markers
One marker per element; most specific wins:
| Marker | Action |
|---|---|
data-surface-goto-step="<stepId>" | Jump to a step. Forward jumps save; jumping back to a visited step rewinds without a write. |
class="surface-disqualify-button" | End the form as disqualified. |
class="surface-submit-button" | Finish and submit. |
class="surface-next-button" | Advance to the next step in record order. |
A <form> container's native submit event also finishes the form (the SDK calls preventDefault). A goto to an unknown step logs a console warning, never throws.
A static branch is two buttons:
<section data-step-id="step_role">
<label>Your role <input data-question-id="q_role" /></label>
<button type="button" data-surface-goto-step="step_company">I'm a buyer</button>
<button type="button" class="surface-disqualify-button">Just browsing</button>
</section>Code-driven Navigation
Dynamic branching calls the handle's methods from your own handlers:
continueButton.addEventListener("click", () => {
const size = form.state().answers["step_company"]?.["q_size"]?.selected;
if (size === "1-10") form.disqualify();
else form.goToStep("step_booking");
});When navigation or submit is code-driven, put data-surface-nav="js" on any element (usually <body>) so validate_form_html does not warn about missing buttons.
Timing Semantics
- Steps switch instantly.
next()andgoToStep()resolve on the step change; the partial save runs behind them and reports throughsavedanderror. Do not disable the continue button for a partial save, and do not insert async work before the step change unless the owner asked for a gate (see email validation). - Finishing is the one wait.
submit()anddisqualify()await the finishing write before the ending step shows. The binding layer disables the clicked button and marks the containersurface-busy. If you callsubmit()without a marker, disable your own button until it resolves.
Endings
After the terminal write, submit() reveals the step with endStepKind: "thank_you" and disqualify() reveals the "disqualified" one. Put the submit button on the last question step and give both endings a container, or the visitor finishes on a blank screen.
Delivery Guarantees
- The finishing write is awaited; a failure emits
error. The server never demotes a finished response: a late partial save is merged, not un-finished. - Partial saves never block navigation. The first forward move creates the response (settling
responseId); the rest are fire-and-forget beacons. - Before programmatic teardown (test harness closing the browser),
await form.flush(). Ordinary visitors need nothing; beacons survive page unloads.