# Theme forms

Source: https://support.storeconnect.com/articles/theme-forms · Last modified 21 August 2026

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 }}
  <input name="{{ form.field_name.name }}" value="{{ form.field_name.value }}">
  <button type="submit">Submit</button>
{% 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

<form action="/products/abc123/add" method="post"
      class="SC-ProductCard_action"
      id="add-to-cart-form"
      data-cart-form="true">
  <input type="hidden" name="authenticity_token" value="...">
  ...
</form>
```

**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 %}

  <label for="email">{{ form.username.label }}</label>
  <input type="email"
         id="email"
         name="{{ form.username.name }}"
         value="{{ form.username.value }}"
         {% if form.errors.size > 0 %}aria-invalid="true"{% endif %}
         required>

  <label for="password">{{ form.password.label }}</label>
  <input type="password"
         id="password"
         name="{{ form.password.name }}"
         required>

  <button type="submit">Log in</button>
{% 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

1. User fills in the form and clicks submit.
2. Browser sends a POST request with `application/x-www-form-urlencoded` data, including the `authenticity_token`.
3. The platform verifies the CSRF token.
4. The platform validates the data.
5. **On success:** the platform performs the action and redirects, usually with a flash notice.
6. **On failure:** the platform re-renders the page with the form pre-filled and `form.errors` populated.

## 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 %}
  <input type="hidden" name="{{ form.variant_id.name }}" value="{{ product.default_variant.id }}">

  <label for="quantity">Quantity</label>
  <input type="number" id="quantity" name="{{ form.quantity.name }}" value="1" min="1">

  <button type="submit">Add to cart</button>
{% endform %}
```


### Login


```liquid

{% form "login", class: "login-form" %}
  {% render "form_errors", errors: form.errors %}

  <div class="field">
    <label for="email">{{ form.username.label }}</label>
    <input type="email" id="email" name="{{ form.username.name }}" value="{{ form.username.value }}" required>
  </div>

  <div class="field">
    <label for="password">{{ form.password.label }}</label>
    <input type="password" id="password" name="{{ form.password.name }}" required>
  </div>

  <button type="submit">Log in</button>

  <p><a href="/password/forgot">Forgot password?</a></p>
  <p><a href="/register">Create account</a></p>
{% 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">
      <label>{{ question.label }}</label>
      {% case question.question_type %}
      {% when "text" %}
        <input type="text" name="{{ question.input_name }}" value="{{ question.answer_value }}">
      {% when "text_area" %}
        <textarea name="{{ question.input_name }}">{{ question.answer_value }}</textarea>
      {% when "picklist" %}
        <select name="{{ question.input_name }}">
          {% for option in question.picklist_values %}
            <option value="{{ option }}" {% if question.answer_value == option %}selected{% endif %}>{{ option }}</option>
          {% endfor %}
        </select>
      {% when "date" %}
        <input type="date" name="{{ question.input_name }}" value="{{ question.answer_value }}">
      {% endcase %}
    </div>
  {% endfor %}
  <button type="submit">Submit</button>
{% 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

1. Add custom hidden inputs or visible fields inside any `{% form %}` block.
2. The form submits them alongside the standard fields.
3. The platform's built-in form handler processes the standard fields and ignores the extras.
4. A Liquid controller (`before` / `after` / `final`) can read the extras via `current_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 %}
  <input type="hidden" name="{{ form.variant_id.name }}" value="{{ current_product.default_variant.id }}">
  <input type="number" name="{{ form.quantity.name }}" value="1" min="1">

  <label>Gift message (optional)</label>
  <textarea name="gift_message" maxlength="200"></textarea>

  <button type="submit">Add to cart</button>
{% 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" %}
  <input type="hidden" name="js_validation_failed" id="js-validation" value="">
  {% render "checkout/shipping_information/form", form: form %}
  <button type="submit">Continue</button>
{% 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.params` contains all submitted form data as a Map, including your custom fields.
- `before` runs first — use it to validate or redirect before the standard action.
- `after` runs second — use it to persist extra data after the standard action succeeds.
- `{% redirect %}` stops execution — once a redirect is issued in `before`, the standard action and `after` are 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) |

---

## Follow StoreConnect

- [Email Newsletter](https://getstoreconnect.com/c/lp-newsletter)
- [LinkedIn Newsletter](https://www.linkedin.com/build-relation/newsletter-follow?entityUrn=7444956928444862464)
- [YouTube](https://www.youtube.com/channel/UCngKdP2x8l1wcbAKW3tvU8g)
- [LinkedIn](https://www.linkedin.com/company/storeconnect)
- [X / Twitter](https://x.com/storeconnecthq)

## Popular Links

- [Partners](https://getstoreconnect.com/partners)
- [News](https://getstoreconnect.com/articles/news)
- [Events](https://getstoreconnect.com/articles/events)
- [Feature Comparison](https://getstoreconnect.com/how-we-compare)
- [Download a free trial](https://appexchange.salesforce.com/appxListingDetail?listingId=a0N3A00000FMkeKUAT)
- [Book a Demo](https://getstoreconnect.com/contact)

## Documentation

- [Help documentation](https://support.storeconnect.com/help-documentation)
- [AI agents](https://support.storeconnect.com/ai)
- [Videos & tutorials](https://support.storeconnect.com/videos-tutorials)
- [Developer reference](https://support.storeconnect.com/developer-reference)
- [Release notes](https://support.storeconnect.com/release-notes)
- [Troubleshooting](https://support.storeconnect.com/troubleshooting)
- [Trust Center](https://trust.getstoreconnect.com/)
- [Status Page](https://status.storeconnect.com/)

## Contact

- info@getstoreconnect.com
- US +1 415 745 3230
- AUS +61 2 8365 2308

100 S Ashley Dr, Suite 600-2461
Tampa FL 33602-600 USA

Level 22, Sydney Place
180 George Street
Sydney, NSW, 2000, AUS

---

StoreConnect Support — https://support.storeconnect.com/articles/theme-forms