Theme forms
On this page
Forms are the primary interaction mechanism in StoreConnect. The {% form %} tag generates HTML forms with the correct action URL, CSRF protection, and field definitions, so you never need to write a raw <form> element for user interactions.
The form tag
Every user interaction in a theme goes through {% form %}. It builds the <form> element with the right action and method, includes the CSRF token, and exposes a form drop for the fields and errors.
```liquid
{% form “form-type”, id: “form-id”, class: “form-class” %} <label for=”{{ form.fields[“field_name”].id }}”>Field label</label> <input id=”{{ form.fields[“field_name”].id }}” name=”{{ form.fields[“field_name”].name }}” value=”{{ form.fields[“field_name”].value }}”> {% endform %} ```
This article covers what is specific to building a theme around forms: the HTML you get, CSRF in a theme layout, and extending a standard form with your own parameters. For the tag itself, see the form tag reference.
HTML attributes on forms
Any option that is not consumed by the form type itself becomes an HTML attribute on the generated <form> element. This includes id, class, and data-* attributes:
```liquid
{% form “add-to-cart”, product_id: product.id, class: “SC-ProductCard_action”, id: “add-to-cart-form”, data-cart-form: true %} … {% endform %} ```
Generates:
```html
```
Common HTML options:
| Option | Example | Purpose |
|---|---|---|
class |
class: "SC-Panel" |
CSS class on the form |
id |
id: "checkout-form" |
HTML id attribute |
data-* |
data-cart-form: true |
Custom data attributes for JavaScript hooks |
Reserved options (consumed internally, not passed to HTML): url, method, format, scope, model, authenticity_token, local, builder, data, html, remote, data-remote.
Form-specific options (consumed by the form type): For example, product_id for add-to-cart, provider for payment forms, custom_form for custom forms. These are extracted by the form handler and do not appear as HTML attributes.
The form drop and its errors
The form drop gives you fields, errors and path, and nothing else: a field is always reached as form.fields["name"], never as form.name. The form tag reference documents the drop, the field attributes and the shape of a FormError, and the Liquid forms guide covers displaying errors on the page.
CSRF protection
All forms require a CSRF token. The {% form %} tag includes it automatically as a hidden authenticity_token field. Your layout must include {{ csrf_meta_tags }} in the <head> for AJAX requests:
```javascript
const token = document.querySelector(‘meta[name=”csrf-token”]’).content;
fetch(‘/cart/items’, { method: ‘POST’, headers: { ‘X-CSRF-Token’: token, ‘Content-Type’: ‘application/x-www-form-urlencoded’ }, body: new URLSearchParams({ product_id: ‘123’, quantity: ‘1’ }) }); ```
:::warning
Never write a raw <form> element for user interactions. Without the {% form %} tag, the CSRF token is missing and all submissions will fail with a security error.
:::
Form submission flow
A submission is validated server-side, then either redirects on success or re-renders the page with form.errors populated and the customer’s values preserved. The Liquid forms guide walks through that cycle and what it means for your markup.
Form types and worked patterns
The Liquid forms reference lists every form type with its fields and parameters, and the Liquid forms guide has worked patterns for the common ones, including add to cart, login, contact and CMS custom forms.
Advanced: extending forms with custom parameters
You can add any extra <input> elements inside a {% form %} block. The standard form handler ignores unrecognized fields, but they are still submitted and available via current_request.params in Liquid controllers. This opens up powerful patterns for custom logic.
How it works
- Add custom hidden inputs or visible fields inside any
{% form %}block. - The form submits them alongside the standard fields.
- The platform’s built-in form handler processes the standard fields and ignores the extras.
- A Liquid controller (
before/after/final) can read the extras viacurrent_request.params.
Example: post-action data capture
Capture a gift message from the add-to-cart form and save it to the cart:
```liquid
{% form “add-to-cart”, product: current_product %} <input type=”number” name=”{{ form.fields[“quantity”].name }}” value=”1” min=”1”>
{% endform %} ```
In a Liquid controller (controllers/carts/add.liquid):
```liquid
{% after %} {%- assign params = current_request.params -%} {% if params.gift_message != blank %} {% update current_cart, field: “gift_message__c”, value: params.gift_message %} {% endif %} {% endafter %} ```
Example: client-side validation with server redirect
JavaScript sets a hidden input to flag an invalid state. The before controller checks it and redirects with an error before the form is processed:
```liquid
{% form “checkout-shipping-information” %} {% render “checkout/shipping_information/form”, form: form %} {% endform %} ```
In controllers/checkout/steps/shipping/update.liquid:
```liquid
{% before %} {%- assign params = current_request.params -%} {% if params.js_validation_failed != blank %} {% redirect to: “/checkout/shipping_information”, alert: “Please enter a valid shipping address” %} {% endif %} {% endbefore %} ```
Key points for custom parameters
- Any input name works — the platform ignores fields it does not recognize, so extra inputs are safe.
current_request.paramscontains all submitted form data as a Map, including your custom fields.beforeruns first — use it to validate or redirect before the standard action.afterruns second — use it to persist extra data after the standard action succeeds.{% redirect %}stops execution — once a redirect is issued inbefore, the standard action andafterare skipped.
The {% redirect %} tag
```liquid
{% redirect to: “/path” %} {% redirect to: “/path”, notice: “Operation completed” %} {% redirect to: “/path”, alert: “Something went wrong” %} {% redirect to: “/path”, status: 301 %} ```
| Option | Description |
|---|---|
to |
URL path to redirect to (required) |
notice |
Flash notice message (informational) |
alert |
Flash alert message (error) |
status |
HTTP status code (default: 302) |
Was this article helpful?
Thanks for your feedback! It helps us improve our docs.