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
```liquid
{% form “form-type” [, option: value, id: “form-id”, class: “form-class”] %} {{ form.field_name.label }} {% endform %} ```
The {% form %} tag automatically:
1. Creates a <form> HTML element with the correct action URL and method.
2. Includes a hidden authenticity_token field for CSRF protection.
3. Makes a form drop available inside the block with field definitions and errors.
4. Passes through extra options as HTML attributes on the <form> element.
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
Inside a {% form %} block, the form variable provides:
| Property | Type | Description |
|---|---|---|
form.errors |
Array | Validation error messages |
form.[field_name] |
FieldDrop | Access to individual form fields |
form.[field_name].name |
String | The input name attribute value |
form.[field_name].value |
Any | The current or default value |
form.[field_name].label |
String | Human-readable label |
form.[field_name].id |
String | HTML id attribute |
form.[field_name].errors |
Array | Field-specific errors |
Error handling
After a failed form submission, the form is re-displayed with errors. The form.errors array contains error messages:
```liquid
{% form “login” %} {% render “form_errors”, errors: form.errors %}
<input type=”email” id=”email” name=”{{ form.username.name }}” value=”{{ form.username.value }}” {% if form.errors.size > 0 %}aria-invalid=”true”{% endif %} required>
{% endform %} ```
StoreConnect stashes form data on failed submissions so the page re-renders with fields pre-filled and form.errors populated. This happens automatically with {% form %}.
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
- User fills in the form and clicks submit.
- Browser sends a POST request with
application/x-www-form-urlencodeddata, including theauthenticity_token. - The platform verifies the CSRF token.
- The platform validates the data.
- On success: the platform performs the action and redirects, usually with a flash notice.
- On failure: the platform re-renders the page with the form pre-filled and
form.errorspopulated.
Form types by category
Every form type StoreConnect registers, grouped the way the platform groups them.
Use the name exactly as shown. {% form %} resolves the action URL, method, and
CSRF token for you, so a theme never needs to know the endpoint.
Accounts
| Form type | Purpose |
|---|---|
register |
Create an account |
account |
Edit the signed-in account |
account-missing-details |
Supply details missing from an account |
forgot-password |
Request a password reset |
reset-password |
Set a new password |
resend-confirmation |
Resend the confirmation email |
accept-invitation |
Accept an invitation to an account |
Session
| Form type | Purpose |
|---|---|
login |
Sign in |
sso-login |
Sign in through single sign-on |
single-sign-on |
Alias of sso-login; both resolve to the same form |
Cart
| Form type | Purpose |
|---|---|
add-to-cart |
Add a product to the cart |
add-bundle-to-cart |
Add a configured bundle to the cart |
add-preset-bundle |
Add a preset bundle to the cart |
cart |
Update quantities or remove items |
Checkout
| Form type | Purpose |
|---|---|
checkout-customer-information |
Customer details step |
checkout-shipping-information |
Shipping method step |
checkout-accept-terms |
Terms acceptance step |
checkout-set-password |
Set a password during checkout |
payment |
Submit payment |
payment-not-required |
Complete an order with nothing to pay |
apply-promo-code |
Apply a promotion code |
remove-promo-code |
Remove an applied promotion code |
apply-voucher |
Apply a voucher |
remove-voucher |
Remove an applied voucher |
activate-voucher |
Activate a voucher |
apply-account-credit |
Apply account credit |
remove-account-credit |
Remove applied account credit |
Payments and subscriptions
| Form type | Purpose |
|---|---|
subscription-payment |
Pay a subscription |
update-subscription-payment-details |
Change the card a subscription bills to |
additional-payment-billing-address |
Billing address for an additional payment |
Bookings
| Form type | Purpose |
|---|---|
booking-attendee-add |
Add an attendee to a booking |
booking-attendee-edit |
Edit a booking attendee |
Privacy
| Form type | Purpose |
|---|---|
privacy-settings |
Save cookie and privacy choices |
privacy-accept-all |
Accept all cookie categories |
privacy-reject-all |
Reject all optional cookie categories |
Geolocation
| Form type | Purpose |
|---|---|
geolocation-select |
Choose a location |
geolocation-dismiss |
Dismiss the location prompt |
Custom forms
| Form type | Purpose |
|---|---|
custom-form |
Submit a Custom Form defined in Salesforce |
Common form patterns
Add to cart
```liquid
{% form “add-to-cart”, product_id: product.id %}
{% endform %} ```
Login
```liquid
{% form “login”, class: “login-form” %} {% render “form_errors”, errors: form.errors %}
{% endform %} ```
Contact form
There is no built-in contact form type. Build the form as a Custom Form in
Salesforce, then render it with custom-form as shown below.
Custom form
Custom forms are defined in the CMS and rendered from a form object:
```liquid
{% form “custom-form”, custom_form: my_form %} {% for question in my_form.questions %} <div class="field"> {% case question.question_type %} {% when “text” %} {% when “text_area” %} {% when “picklist” %} {% when “date” %} {% endcase %} </div> {% endfor %} {% endform %} ```
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_id: current_product.id %}
{% 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.