Form - Liquid Tag Reference
On this page
The {% form %} block tag generates an HTML form with built-in handling for submissions and validation. It automatically wraps content in a <form> element, includes CSRF protection, and provides a form drop that exposes fields, their attributes, and validation errors.
Syntax
```liquid
{% form “form-type” [, option: value, …] %} {% endform %} ```
| Property | Value |
|---|---|
| Tag Name | form |
| Type | Block tag |
| Source | Hydrofoil (core) |
Description
The form tag provides form rendering and submission handling in StoreConnect Liquid templates. It handles:
- Automatic form wrapping with correct action, method, and CSRF protection
- Field definitions and accessibility attributes through the form drop
- Validation error tracking and display
- Submission state preservation for re-displaying user input on validation failure
For comprehensive usage patterns, see Liquid forms guide.
Form drop structure
Inside a {% form %} block, you can access a form drop with the following attributes:
Form drop attributes
| Attribute | Type | Description |
|———–|——|————-|
| fields | Collection[FormField] | Every field the form type defines, looked up by name as form.fields["field_name"] |
| errors | Collection[FormError] | Every validation error on the form, including those not tied to a single field |
| path | String | The form’s submission URL path |
fields, errors and path are the whole drop. There is no shorthand attribute for a field.
Field access pattern
Every field is reached through fields, keyed by the field name:
```liquid
{{ form.fields[“email”].value }} ```
A bare form.username is not a field. FormDrop defines no such attribute, so the expression renders as an empty string and the rest of the page renders normally. The failure is silent in the page: nothing marks the spot, and undefined method username is reported only to the Console.
The key can be a variable, so a field name can be built at render time:
```liquid
{% assign field_name = “email” %} {{ form.fields[field_name].value }} ```
Field attributes
Each field in a form has the following attributes:
| Attribute | Type | Description |
|---|---|---|
name |
String | The HTML name attribute for the field. Use this in <input> elements. |
id |
String | The HTML id attribute for the field (if assigned by the form) |
value |
String | The current value of the field. Preserved on re-render after validation failure. |
original_value |
String | The value of the field before the current request (useful for detecting changes) |
required? |
Boolean | Whether the field is required |
errors |
Array | Array of error messages specific to this field |
A field carries no label. Write the label text yourself and wire it to the field with for and id.
Field rendering example
```liquid
```
Error handling
form.errors is a collection of FormError objects: one per field that failed, plus one carrying the field name base for errors that belong to the form as a whole. It is not a list of strings.
| On a FormError | Type | Holds |
|---|---|---|
field |
String | The field the error belongs to, or base for the form itself |
messages |
Collection[String] | The messages, without the field name |
full_messages |
Collection[String] | The messages, each prefixed with the field name |
Because each error holds a list, rendering them takes two loops:
```liquid
{% for error in form.errors %} {% for message in error.full_messages %} <li>{{ message }}</li> {% endfor %} {% endfor %} ```
Outputting {{ error }} directly renders the drop, not the message.
form.errors.size is the test for whether a submission failed. A field carries its own form.fields["email"].errors, but that is a single FormError rather than a collection, so test it with != blank rather than .size, which raises on it.
Field values survive a failed submission through form.fields["email"].value, so a re-rendered form keeps what the customer typed.
For the display patterns built on this, see Liquid forms guide.
Form parameters
When creating a form, you can pass options that control the form’s behavior:
Standard parameters
| Parameter | Type | Description |
|———–|——|————-|
| product | ProductDrop | The product for add-to-cart forms (preferred over product_id) |
| product_id | String | The product ID for add-to-cart forms (use product instead when possible) |
| id | String | HTML id attribute for the <form> element |
| class | String | HTML class attribute for the <form> element |
| data-* | String | Custom data attributes are passed through to the form element |
Form-specific parameters
Different form types accept different parameters. For example:
```liquid
{% form “remove-voucher”, voucher: current_voucher %} {% endform %}
{% form “add-to-cart”, product: current_product, id: “ProductForm” %} {% endform %}
{% form “checkout-customer-information”, class: “form-compact” %} {% endform %} ```
Registered form names
StoreConnect provides many built-in form types. See Liquid forms reference for the complete list, including:
- Authentication:
login,register,forgot-password,reset-password,accept-invitation - Cart & Checkout:
add-to-cart,cart,checkout-customer-information,checkout-shipping-information,payment - Promotions:
apply-voucher,remove-voucher,apply-promo-code - Account:
account,account-missing-details - Custom:
custom-form(for CMS-defined forms) - Other:
privacy-settings,geolocation-select,booking-attendee-add, and more
Common form patterns
### Pattern 1: basic form with field rendering
```liquid
{% form “register” %}
{% endform %} ```
Pattern 2: error display
```liquid
{% form “login” %} {% render “form_errors”, errors: form.errors %}
{% assign username = form.fields[“username”] %}
{% endform %} ```
Pattern 3: add to cart with product context
```liquid
{% form “add-to-cart”, product: current_product, id: “AddToCartForm” %}
{% render “form_errors”, errors: form.errors %}
{% endform %} ```
Pattern 4: custom form from CMS
```liquid
{% if my_contact_form %} {% form “custom-form”, custom_form: my_contact_form, id: “ContactForm” %} {% for question in my_contact_form.questions %} <div class="form-group {% if question.is_required %}required{% endif %}">
{% case question.question_type %}
{% when "text" %}
<input
type="text"
id="q_{{ question.id }}"
name="{{ question.input_name }}"
value="{{ question.answer_value }}"
{% if question.is_required %}required{% endif %}>
{% when "text_area" %}
<textarea
id="q_{{ question.id }}"
name="{{ question.input_name }}"
{% if question.is_required %}required{% endif %}>{{ question.answer_value }}</textarea>
{% when "picklist" %}
<select
id="q_{{ question.id }}"
name="{{ question.input_name }}"
{% if question.is_required %}required{% endif %}>
<option value="">-- Select --</option>
{% for option in question.picklist_values %}
<option value="{{ option }}" {% if question.answer_value == option %}selected{% endif %}>
{{ option }}
</option>
{% endfor %}
</select>
{% endcase %}
</div>
{% endfor %}
{% render "form_errors", errors: form.errors %}
<button type="submit">Send</button> {% endform %} {% endif %} ```
Was this article helpful?
Thanks for your feedback! It helps us improve our docs.