> For the complete documentation index, see [llms.txt](https://analog.groupthought.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://analog.groupthought.com/reference/alpine/product-form.md).

# x-product-form

Directives and a magic for product forms: add to cart with fetch, with cart sections updated in place.

Directives and a magic for product forms: add to cart with fetch, with cart sections updated in place.

| Directive / magic                  | What it does                                                                                                                                              |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `x-product-form`                   | The root, on the `{% form 'product' %}` element or a parent of one                                                                                        |
| `x-product-form.wait-error.3000ms` | Modifiers pass through to `x-async-form`: `wait-success` (default 1000ms) and `wait-error`                                                                |
| `x-product-form:context`           | Holds the form state. The root binds it for you                                                                                                           |
| `x-product-form:form`              | The form element. Sets `method="POST"`. The root binds it for you                                                                                         |
| `$productForm`                     | The same API as [`$asyncForm`](/reference/alpine/async-form.md): `state`, `parsedBody`, `errorMessage`, `errorDescription`, `submitAsync()`, `reset()`, … |

Submission goes to `/cart/add.js` (`window.theme.routes.cartAdd`). The plugin appends the ids of every section marked `data-cart-api-section` to the request's `sections` parameter. The response includes updated HTML for the cart drawer, Cart page, and header bubble, which re-render without a page reload.

On top of the [`x-async-form` events](/reference/alpine/async-form.md), the plugin dispatches the standard `shopify:cart:lines-update` event (action `'add'`) before the request, with `event.promise` resolving to the cart and theme values `sections`, `items`, `item` (the added line), `source`, and `didError`. It dispatches `shopify:cart:error` on failure. See [Events](/developer-platform/events.md). The theme listens for the lines-update event to open the cart drawer, show the notification, or go to the cart page, following the "Cart action" theme setting.

## Examples

### A basic product form

Use `x-product-form` on the product form. Card quick-add buttons submit this way:

```liquid
{% form 'product', product, x-product-form: '' %}
  <input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">
  <input type="hidden" name="quantity" value="1">
  <button type="submit">Add to cart</button>
{% endform %}
```

Without JavaScript the form submits normally to `/cart/add` and Shopify renders the cart page.

### Button states

Use `$productForm.state` to swap the button label while the request runs:

```html
<button type="submit" :disabled="$productForm.state === 'pending'">
  <span x-show="$productForm.state === 'idle'">Add to cart</span>
  <span x-show="$productForm.state === 'pending'">Adding…</span>
  <span x-show="$productForm.state === 'success'">Added</span>
</button>
```

The success state returns to idle after the `wait-success` timeout (default 1000ms).

### Showing errors

Use `$productForm.errorDescription` with `$productForm.errorMessage` as the fallback, and `reset()` to clear the error when the variant changes:

```html
<div
  role="alert"
  x-show="$productForm.state === 'error'"
  @shopify:product:select.window="if ($productForm.state === 'error') $productForm.reset()"
  x-cloak
>
  <p x-html="$productForm.errorDescription || $productForm.errorMessage"></p>
</div>
```

A stockout returns a 422 whose body message becomes `errorMessage`, and the error state stays until the next submission or the reset.

### Wrapping a whole block

Use `x-product-form` on a parent element to share `$productForm` with markup outside the form. The product details column does this so the add buttons, error block, and quantity stepper all read one state:

```liquid
<div x-product-form>
  <p role="alert" x-show="$productForm.state === 'error'" x-text="$productForm.errorMessage"></p>

  {% form 'product', product %}
    …
  {% endform %}
</div>
```

The plugin finds the first `<form>` inside the wrapper and binds the form directive to it.

## Customizing

The merchant's "Cart action" setting opens the drawer, shows a notification, or navigates to the Cart page after a successful add. See [Cart](/features/cart.md) for that behavior and [Product](/features/product.md) for the product form's blocks and settings.
