The Binding Contract
The data attribute vocabulary that connects your HTML to the form record
The binding layer discovers your inputs through four data attributes, captures answers as visitors type, and shows one step at a time. Exact IDs and answer shapes come from the get_sdk_binding_map MCP tool; never invent them.
The four attributes
data-question-id
On the input itself or on a wrapper; every input, select, and textarea inside a wrapper binds to that question. Bind every answer-recording question; the SDK warns in the console about unbound ones. A control inside a nested wrapper belongs to the inner question only.
Schedulers (CalendlyScreen, DynamicScheduler) and FileUploader need no HTML binding; their answers are written in code, and validate_form_html skips them.
data-field-name
Which key of the component's answer object this control writes. Resolution: wrapper's attribute, control's attribute, then inference ("selected" for a <select> on a Dropdown, "email" for input[type=email], "input" otherwise), and inference only works when the control type matches the component. An email input bound to a ShortInput needs an explicit data-field-name="input"; every control of a multi-field component needs its own explicit name:
<div data-question-id="q_contact" data-question-type="IdentityInfo">
<input data-field-name="firstName" placeholder="First name" />
<input data-field-name="lastName" placeholder="Last name" />
<input type="email" data-field-name="workEmailAddress" placeholder="Work email" />
</div>IdentityInfo field names are a closed set: emailAddress and workEmailAddress (not email), phoneNumber (not phone), firstName, lastName, companyName, websiteUrl, title, numberOfEmployees. Bind only the keys the binding map lists.
data-question-type
The record's component type. Inferred only for ShortInput (the default) and EmailForm (email inputs); every other type must be set explicitly or the server rejects the answer.
data-step-id
One container per record step; the SDK shows exactly one at a time by setting hidden on the rest. Single-step records may omit containers. Author every container after the first with the hidden attribute; unhidden steps flash stacked while init resolves.
Value capture
- Text controls contribute
value; checkboxes contributechecked; radios follow the choice group rules below. - Values already in the DOM at bind time (server-side rendering, autofill) are captured immediately.
- Multi-field components accumulate one answer object per question, e.g.
{ firstName, lastName, workEmailAddress }.
Choice questions bind as a group
Five types are group-built: MultipleOptionsQuestion, MultipleChoiceQuestion, MMChoiceQuestion, GraphicOptions, MultipleChoiceSingleAnswer. Their answers are structured objects built from the container's whole radio or checkbox group:
<fieldset data-question-id="q_role" data-question-type="MultipleOptionsQuestion">
<label><input type="radio" name="role" value="Revenue / Sales Ops" /> Revenue / Sales Ops</label>
<label><input type="radio" name="role" value="Sales leadership" /> Sales leadership</label>
</fieldset>data-question-typeon the container is required; without it the server rejects the answer.- Each control's
valuemust equal the record's option key exactly (validate_form_htmlcross-checks). - Never set
data-field-nameon the options. - Author controls in record order. Multi-select uses checkboxes with the same markup.
The five types have different answer shapes (only MultipleOptionsQuestion carries an option index). In bound HTML the SDK builds them; in code, copy each question's answerShape from the binding map.
MultipleChoiceQuestion and MultipleChoiceSingleAnswer are legacy types. When porting, create a MultipleOptionsQuestion with isMultiSelect true or false respectively.
The "Other" option
The group captures the option selection only. Render an extra text control outside the group and mark the answer when used:
otherInput.addEventListener("input", () => {
const current = form.state().answers["step_role"]?.["q_role"] ?? {};
form.setAnswer("q_role", { ...current, selectedOther: true });
});The typed text has no slot on the choice answer; if the owner wants it stored, add a ShortInput question to the record.
Dropdowns
A Dropdown binds a <select> whose option values are the record's option keys; the state key (selected) is inferred:
<label>
Company size
<select data-question-id="q_size" data-question-type="Dropdown">
<option>1-10</option>
<option>11-50</option>
</select>
</label>Busy state during submit
While submit() or disqualify() wait on the finishing write, the binding layer marks the container with the surface-busy class, sets aria-busy="true" and a progress cursor, and disables the clicked button. Style .surface-busy for a spinner; nothing else is needed.
Validating your markup
Run the validate_form_html MCP tool on the markup visitors actually receive; it checks every rule on this page against the record. For a framework page, validate the rendered HTML from your dev server or build output, never a hand transcription. Details in Building with AI.