The form is built in the CMS; leed/form renders it and wires it to submission handling. Field types, labels, options and validation are chosen in the Forms builder — this page documents what each of those choices emits, so you can style it, place it, and know what will arrive on the other side.
Forms are free on every plan. There is no tier gate on building one, rendering one, or receiving what it collects.
Calling it
{{> leed/form formId="72339fba" }}:::warning There is no {{> leed-form }} The partial is leed/form. leed-form is a CSS class on the <form> element. An include that names a partial which does not exist renders as an empty string, with no error — so a typo here looks exactly like a form that failed to load. :::
There are three ways a form reaches a page, and which one you use depends on who decides where the form goes.
- Page setting
- Form block
- Template partial
Omit formId entirely and the partial reads it from the page’s own data:
{{> leed/form }}This is the landing-page pattern. The layout is fixed, and an editor chooses which form appears on each page from the CMS without touching a template. Use it when the form’s position is a design decision and the form’s identity is an editorial one.
An editor drops a Form block into the page body, which stores a partial call inside the content itself:
{{> leed/form formId="72339fba" }}That call reaches Handlebars only because {{{ process content }}} compiles embedded partial calls found in rendered page content. It is the one exception to the rule that CMS page bodies are not template-processed — see How Templates Work. The editor-facing side is at Placing a Form on a Page.
Name the form in the template and every page using that layout gets it:
<aside class="sidebar-cta">
{{> leed/form formId="72339fba" }}
</aside>Use it for a newsletter box in a footer or a demo request in a sidebar — anywhere the same form belongs on many pages and no editor should have to remember to add it.
What it needs
The partial reads three things out of the build’s data:
leedForms, fromsrc/_data/leedForms.json— CMS-written, one entry per published form, keyed byformId. The whole file is replaced on every form publish, so hand-editing it does not survive.leed-form-data.countries— Leed’s own country list, used bycountriesfields. It ships with the site builder rather than coming from your repository.@root, for the Turnstile site key.
The container
Top to bottom, this is what wraps every form:
<div class="leed-form-container inline-form-container">
<div id="72339fba-heading" class="leed-form-heading">Talk to us</div>
<div id="72339fba-wb" style="display: none;" class="leed-form-welcome-back">Welcome back, Ada.</div>
<div id="72339fba-details" class="leed-form-details">We reply within one business day.</div>
<form data-id="72339fba" class="LeedForm leed-form inline-form" id="72339fba">
…
</form>
</div>The heading, welcome-back and details divs each appear only when the form defines the matching value; the welcome-back div is tied to leedAutofill rather than to its own message, and is hidden until the autofill script recognizes a returning visitor.
The <form> element carries three classes, and the template’s own comment explains why they are separate:
| Class | Who owns it | Purpose |
|---|---|---|
LeedForm | Leed | The submit hook. The bundled JavaScript attaches to the submit event of every element with this class. Remove it and the form posts nowhere. |
leed-form | you | Pure styling. This is the class to write CSS against. |
<formPrototype> | you | The reusable style name set on the form in the CMS, so one visual treatment can be shared across many forms. The container div also gets <formPrototype>-container. |
id and data-id both carry the formId, and every id inside the form is prefixed with it — which is what lets two different forms coexist on one page.
Field types
Each field is wrapped in <div class="leed-form-block <type> known-valid-hideable">. The known-valid-hideable marker is added to every field except one named terms, which is how a consent checkbox stays visible when other already-known fields are hidden from a returning visitor.
type | Emits | Options honored | Notes |
|---|---|---|---|
text | <input type="text"> | — | placeHolder, pattern, value, oninput, onchange, required |
email | <input type="email"> | — | Same attribute set as text |
tel | <input type="tel"> | — | Same attribute set as text |
textarea | <textarea> | — | rows, cols, multiple; value becomes the element’s initial content |
select | <select> with one <option> per entry | options[] (value, label) | A placeHolder becomes a leading <option value=""> |
countries | <select> populated from Leed’s country list | ignores the field’s own options[] | A matching field value preselects that country. The list is shipped by the site builder, not by your repository. |
checkbox | <input type="checkbox"> | — | selected renders checked="checked"; the id includes the value |
radio | <input type="radio"> | — | Same as checkbox; several fields sharing a name form a group |
label | <label> containing value, then <br> | — | Renders text, not an input. Template-only. |
hidden | <input type="hidden"> | — | Emitted in a second pass after every other field, so hidden inputs always sit at the end of the form. Template-only. |
radiogroup | one <div class="leed-form-group-option"> per option, each holding an <input> | options[] (value, labelBefore, labelAfter, selected) | Template-only, and see the warning below |
checkboxgroup | the same structure as radiogroup | as radiogroup | Template-only, and see the warning below |
Form definition fields
These are the keys on a form that the partial reads. They are exactly the fields the CMS writes into leedForms.json; a form’s internal name and description stay in the CMS and never reach the site.
| Field | Effect |
|---|---|
heading | Renders #<formId>-heading. Escaped. |
details | Renders #<formId>-details below the heading. Escaped. |
formPrototype | Adds <value> to the <form> class list and <value>-container to the wrapper |
leedAutofill | Emits the hidden #<formId>-wb div and a leedAutofill('<formId>') call after the form |
welcomeBackMessage | The text inside that welcome-back div |
successCallback | Written as data-successcallback on the <form> |
failureCallback | Written as data-failurecallback on the <form> |
autofill | "linkedin" loads LinkedIn’s autofill script and maps the recognized fields to it |
button | The submit button’s default label. Emitted raw, so an icon in the label works. |
fields[] | The fields, in order |
Field object fields
| Field | Applies to | Effect |
|---|---|---|
type | all | Selects the branch above |
name | all | The submitted key, and half of the element’s id |
value | all | Initial value; textarea uses it as content; radio/checkbox use it as the submitted value and in the id; countries uses it to preselect a country |
required | all inputs | Emits required="true" |
multiple | select, countries, textarea | Emits multiple="true" |
size | select, countries | Emits size="…". Read by the template; the Forms builder does not set it. |
rows | textarea | Emits rows="…" |
cols | textarea | Emits cols="…". Read by the template; the Forms builder does not set it. |
pattern | text, tel, email, radio, checkbox | Emits a pattern attribute for native validation |
placeHolder | text, tel, email, radio, checkbox | A placeholder attribute. On select and countries it instead becomes a leading <option value="">. |
oninput | text, tel, email, textarea, radio, checkbox | Inline handler |
onchange | the same set, plus the two group types | Inline handler; a group passes its own down to every option |
selected | radio, checkbox | Emits checked="checked" |
labelBefore | all | A <label for="…"> before the control. Emitted raw. |
labelAfter | all | A <label for="…"> after the control. Emitted raw. |
options[] | select, and the two group types | { value, label } per entry |
The complete emitted markup of a four-field form
A form with a heading, details, a formPrototype of inline-form, and four fields — text, email, countries, checkbox. Whitespace normalized; the country list truncated to two entries.
<div class="leed-form-container inline-form-container">
<div id="72339fba-heading" class="leed-form-heading">Talk to us</div>
<div id="72339fba-details" class="leed-form-details">We reply within one business day.</div>
<form data-id="72339fba" class="LeedForm leed-form inline-form" id="72339fba">
<div class="leed-form-block text known-valid-hideable">
<label for="72339fba-firstname">First name</label>
<input id="72339fba-firstname" name="firstname" type="text" placeholder="Ada" required="true">
</div>
<div class="leed-form-block email known-valid-hideable">
<label for="72339fba-email">Work email</label>
<input id="72339fba-email" name="email" type="email" required="true">
</div>
<div class="leed-form-block countries known-valid-hideable">
<label for="72339fba-country">Country</label>
<select id="72339fba-country" name="country">
<option value="">Choose one</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
</select>
</div>
<div class="leed-form-block checkbox">
<input id="72339fba-terms-yes" name="terms" type="checkbox" value="yes">
<label for="72339fba-terms-yes">I agree to the terms</label>
</div>
<button id="72339fba-button" data-default="Send" type="submit" class="flex items-center">
<div id="72339fba-button-submit" class="leed-form-button-label" style="display: block;">Send</div>
<div id="72339fba-button-sending" class="leed-form-button-label" style="display: none;">Sending ...</div>
<div id="72339fba-button-email-sent" class="leed-form-button-label" style="display: none;">Email Sent!</div>
<div id="72339fba-button-error" class="leed-form-button-label" style="display: none;">Try again!</div>
<div id="72339fba-button-thanks" class="leed-form-button-label" style="display: none;">Thank you!</div>
<div id="72339fba-button-downloading" class="leed-form-button-label leed-form-button-spinner !flex items-center" style="display: none !important;">
<div><i class="leed-form-spinner"></i></div>
<div>Downloading ...</div>
</div>
</button>
<div class="cf-turnstile" data-sitekey="0x4AAA…" style="display: none;"></div>
</form>
</div>
<!-- Leed Form generated by https://leed.ai -->The terms block is the one without known-valid-hideable. The <label> sits before the input for the three data fields because they set labelBefore, and after it for the consent checkbox because it sets labelAfter.
Ids and names
Two attributes do two different jobs, and mixing them up is what breaks label clicking:
- The id connects a
<label for="…">to its control. It is generated, never authored. - The name is what gets submitted. It is yours to choose in the Forms builder, and it is what decides whether the value lands in a contact record or not.
The id comes from the fieldId helper: <formId>-<name> for most fields, and <formId>-<name>-<value> for radio and checkbox, so several members of one group stay unique while sharing a name. fieldId is an ordinary helper you can call yourself — see Form and CTA Helpers.
The names that become a contact
This is the most useful table on the page if you are hand-building a form or renaming fields. Four names are supplied by Leed at submit time; thirteen more map straight into the contact record if you use them; anything else is captured but unmapped.
| Name | Source | Maps to |
|---|---|---|
formId | added at submit | the form the submission came from |
pid | added at submit | the page it was submitted from |
pt | added at submit | that page’s page type |
u | added at submit | the visitor identifier |
email | your field | contact email |
phone | your field | contact phone |
company | your field | company |
firstname | your field | first name |
lastname | your field | last name |
title | your field | job title |
address | your field | street address |
city | your field | city |
state | your field | state or region |
postalcode | your field | postal code |
country | your field | country |
terms | your field | consent |
productname | your field | product interest |
| anything else | your field | captured with the submission, not mapped to a contact field |
Spelling matters exactly as written — all lower case, no separators. firstName and first_name are both “anything else”. What happens to a submission after it arrives, and why a preview site never records one, is at Form Submissions; the contact record itself is at Contacts.
The submit button
There is one <button>, holding six labeled <div>s. The bundled JavaScript shows one at a time by toggling inline display; you style them.
| Id suffix | Shown when |
|---|---|
-button-submit | at rest — carries the form’s own button text |
-button-sending | the submission is in flight |
-button-email-sent | a response email has been sent |
-button-error | the submission failed |
-button-thanks | the submission succeeded |
-button-downloading | a gated download is being prepared — this one also carries leed-form-button-spinner and an <i class="leed-form-spinner"> |
Each is <formId><suffix>, and each carries the class leed-form-button-label. The button’s own data-default attribute holds the resting label so the script can restore it.
Because the script writes inline display, a CSS rule that sets display on .leed-form-button-label will not win. Style color, spacing and typography there; leave visibility to the script.
Turnstile
Every form ends with an invisible Cloudflare Turnstile widget:
<div class="cf-turnstile" data-sitekey="…" style="display: none;"></div>The site key comes from the turnstileKey helper, which reads deployment.preview.turnstilePublicKey on a preview build and deployment.public.turnstilePublicKey otherwise. If the relevant key is not configured, it falls back to Cloudflare’s always-pass test key 1x00000000000000000000BB — so a form still renders and still submits on a site that has never configured Turnstile. Which build is which is covered at Preview Site vs Live Site.
Autofill and callbacks
Leed autofill. With leedAutofill set, the partial emits the hidden #<formId>-wb welcome-back div and a leedAutofill('<formId>') call after the form. On a return visit the script fills the fields it already knows, reveals the welcome-back message, and — with the known-valid-hideable markers described above — hides the fields it has already collected, leaving the consent checkbox visible.
LinkedIn autofill. With autofill: "linkedin", the partial loads LinkedIn’s autofill script and emits an IN/Form2 script tag mapping your fields to LinkedIn’s. Only the names it recognizes are mapped: firstname, lastname, phone, email, company, title, city, state, country, plus postalcode which maps to LinkedIn’s zip. Another reason to use the mapped names from the table above.
Callbacks. successCallback and failureCallback become data-successcallback and data-failurecallback on the <form>. They name functions the submission script calls; they are set on the form in the CMS, described at Building a Form.
leed/unsubscribe-form
The second form partial is much simpler and takes no parameters:
{{> leed/unsubscribe-form }}It emits a static <form id="unsubscribeForm" class="leed-form" onsubmit="return submitLeadUnsubscribe();"> with five radio reasons — Too many emails, Content does not meet my needs or interests, Content was not what I expected, I never signed up for these emails, Other reason — all named reason, plus a donotcontact checkbox and an #unsubscribe-button submit. There is nothing to configure: no CMS form backs it, and the field names are fixed because the unsubscribe endpoint expects them.
It is rendered by Leed’s own unsubscribe page. If you want a different unsubscribe experience, you do not parameterize this partial — you replace the whole page, which is one of the six override slots described at Overriding Leed Templates.
Where leed/form sits among the other fifty-three partials Leed injects, and which of them you may call, is at the Leed Partial Index.