# JavaScript events & actions for developers
Motrix supports Shopify's [Standard Storefront Events & Actions](https://shopify.dev/docs/api/storefront-events-and-actions), a platform-standard JavaScript API for interacting with and responding to storefront activity.
## Choose the right API
The API has two halves, and they answer two different questions:
Use **Actions** when your code needs to make something happen, such as reading or updating the cart.
* Add, update, or remove items from the cart → **Action** (`updateCart`)
* Open the cart → **Action** (`openCart`)
Use **Events** when your code needs to respond to something that happens in the storefront
* Detect when a product is added to the cart → **Event** (`shopify:cart:lines-update`)
* React when a shopper changes filters on a collection → **Event** (`shopify:collection:update`)
If the behavior isn't covered by Shopify's standard events, use the **Theme events** provided by Motrix.
## Actions `Shopify.actions.*`
Actions let your code **tell the storefront to do something**.
They are available on every page without additional setup.
### Action read the cart - `getCart()`
Use `getCart()` to retrieve the current cart.
```js
const { cart } = await Shopify.actions.getCart();
console.log(cart?.totalQuantity);
console.log(cart?.cost.totalAmount.amount);
```
The resolved `cart` is a `CartSummary`. It can be `null` when the shopper doesn't have a cart yet.
| Field | Type | Description |
| --------------- | ----------------- | ------------------------------------------------------------- |
| `id` | `string` | The cart GID, such as `gid://shopify/Cart/...` |
| `totalQuantity` | `number` | Items across all lines |
| `cost` | `{ totalAmount }` | `totalAmount: { amount, currencyCode }` |
| `lines` | `CartLine[]` | Each line: `id` (line GID), `quantity`, `cost`, `merchandise` |
| `discountCodes` | `array` | Discount codes currently applied to the cart |
### Action change the cart - `updateCart(payload)`
Use `updateCart()` to **add**, **update**, or **remove** cart items, or to **update the cart note**, **discount codes**, or **attributes**.
| Field | Type | Meaning |
| --------------- | ----------------- | -------------------------------------------------------------------------------------- |
| `lines` | `CartLineInput[]` | Lines to add/update/remove, check the below section |
| `note` | `string` | The cart note. Pass an empty string to clear it |
| `discountCodes` | `string[]` | The complete set of discount codes. Passing `[]` clears them |
| `attributes` | `array` | The **complete** set of cart attributes (`{ key, value }`), replacing the existing set |
| `cartId` | `string` | Optional — omitted, the action finds the cart from the browser cookie |
Each `CartLineInput`:
* **Add**: `{ merchandiseId, quantity }` — `merchandiseId` is the variant GID (`gid://shopify/ProductVariant/123`) or a raw numeric variant id. No `id`.
* **Update**: `{ id, quantity }` — `id` is the **existing line id** (read it from `getCart().cart.lines[n].id`).
* **Remove**: `{ id, quantity: 0 }`.
```js
// Add a variant
await Shopify.actions.updateCart({
lines: [
{
merchandiseId: "gid://shopify/ProductVariant/123456789",
quantity: 1
}
]
});
// Update a line
const { cart } = await Shopify.actions.getCart();
await Shopify.actions.updateCart({
lines: [
{
id: cart.lines[0].id,
quantity: 3
}
]
});
// Update the cart note
await Shopify.actions.updateCart({
note: "Please gift wrap"
});
// Apply discount codes
await Shopify.actions.updateCart({
discountCodes: ["WELCOME10"]
});
```
Always check `userErrors` and `warnings` in the result. A successful request does not necessarily mean that the cart change was accepted.
```js
const { cart, userErrors, warnings } =
await Shopify.actions.updateCart({
lines: [
{
merchandiseId: "gid://shopify/ProductVariant/123",
quantity: 99
}
]
});
if (userErrors?.length) {
showMessage(userErrors[0].message);
return;
}
if (warnings?.length) {
showMessage(warnings[0].message);
}
updateBadge(cart.totalQuantity);
```
The promise rejects only when the request itself fails, such as a network failure or malformed payload. Use `try/catch` to handle these failures.
* **Cart surfaces update automatically.** You don't need to refresh them manually.
* **Standard events are emitted automatically.** An `updateCart()` call also triggers the corresponding standard events, such as `shopify:cart:lines-update`, with `context: 'standard-action'`. If you're already listening for these events, avoid handling the same cart change twice.
### Action open the cart - `openCart()`
Use `openCart()` to open the cart using the same behavior as the theme's cart button.
```js
await Shopify.actions.openCart();
```
When the cart type is set to **Drawer**, the cart drawer opens. When it's set to **Page**, the shopper is taken to `/cart`.
A common pair: `updateCart(...)` then `openCart()`.
## Standard storefront events - `shopify:*`
Events let your code **respond to something that happens in the storefront**.
All standard events bubble to `document`, so you can listen for them from a single place
```js
document.addEventListener("shopify:cart:lines-update", (event) => {
console.log(event.action, event.context, event.lines);
});
```
There are two types of standard events:
[**View events**](https://docs.foxecom.com/motrix-theme/faqs/javascript-events-and-actions-for-developers#view-events-no-promise)**:** report something that has already happened. They don't include a `promise` because the payload is complete when the event fires.
[**Operation events**](https://docs.foxecom.com/motrix-theme/faqs/javascript-events-and-actions-for-developers#operation-events-with-promise): fire when an operation starts, before the result is available. They include a **`promise`** that settles when the operation finishes:
* The promise **resolves** even if the cart rejects the change. Check `userErrors` in the result to determine whether the operation was accepted.
* The promise **rejects** only when the request itself fails, such as a network error. In this case, `shopify:cart:error` is also dispatched.
### View events (no promise)
| Event | Fires when | Payload |
| ------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `shopify:page:view` | A page loads | `page`— `template`, `title`, `URL` |
| `shopify:product:view` | The main product renders on a product page | `product`—`id` (GID), `title`, `handle`; `context: 'page'` |
| `shopify:collection:view` | The buyer lands on a collection page | `collection`— `id` (GID), `handle`, `productsCount` |
| `shopify:cart:view` | The cart page loads (`context: 'page'`), or the cart drawer opens (`context: 'dialog'`, fires on each open) | `cart`, `context` |
`shopify:page:view` fires as soon as the document is ready. Register the listener synchronously when your script loads, or you may miss the event.
### Operation events (with promise)
| Event | Fires when | Key payload | Promise resolves with |
| ------------------------------ | ----------------------------------------- | ----------------------------------------------- | :--------------------------------------------------------- |
| `shopify:cart:lines-update` | Cart lines are added, updated, or removed | `action`, `context`, `lines` | `{ cart, userErrors, warnings }` |
| `shopify:cart:note-update` | The cart note changes | `note`, `context` | `{ cart, userErrors, warnings }` |
| `shopify:cart:discount-update` | Discount codes change | `discountCodes: [{ code }]` | `{ cart, userErrors, warnings }` |
| `shopify:product:select` | A shopper changes a product option | `product`, `selectedOptions: [{ name, value }]` | `{ variant }` — `null` when the combination has no variant |
| `shopify:collection:update` | A shopper filters or sorts a collection | `collection`, `productFilters`, `sortKey` | `{ productsCount }` |
| `shopify:search:update` | A search runs or its filters/sort change | `search`- `query`, `productFilters`, `sortKey` | `{ totalCount }` |
And the error signal (no promise — it IS the failure notification):
| Event | Fires when | Payload |
| -------------------- | ------------------------------------------------------------------------- | ------------------------------------------- |
| `shopify:cart:error` | A cart request fails (network/server error — never for a declined change) | `error`(message), `code`(a `CartErrorCode`) |
### Event cart changes - `shopify:cart:lines-update`
The `shopify:cart:lines-update` event covers cart line additions, updates, and removals.
* The `action` value tells you what happened: `'add' | 'update' | 'remove'`
* The `context` value tells you where the change came from: `'product'` (product page form), `'cart'` (cart page), `'dialog'` (drawer / quick view), or `'standard-action'` (an `updateCart` call)
* The `lines` entries include `merchandiseId` when adding a product, or `id` when updating or removing an existing line, along with the `quantity`.
```js
document.addEventListener("shopify:cart:lines-update", (event) => {
showSpinner(); // the operation just STARTED
event.promise
.then(({ cart, userErrors, warnings }) => {
if (userErrors?.length) return showMessage(userErrors[0].message); // declined
if (warnings?.length) showMessage(warnings[0].message); // applied with adjustment
updateBadge(cart?.totalQuantity); // cart is null when no cart exists
})
.catch(() => showMessage("We couldn't reach the store. Try again."))
.finally(hideSpinner);
});
```
### Event product variant selection - `shopify:product:select`
Fires as soon as an option changes, before the matching variant is determined.
The promise returns the selected variant with its `id` (GID), `title`, and `price` (`amount` and `currencyCode`).
If the selected combination doesn't match an existing variant, it returns `variant: null`.
```js
document.addEventListener("shopify:product:select", (event) => {
console.log(event.product.handle, event.selectedOptions); // [{ name: "Color", value: "Blue" }, …]
event.promise.then(({ variant }) => {
if (!variant) return markUnavailable();
updatePrice(variant.price.amount, variant.price.currencyCode);
});
});
```
### Event collection and search update - `shopify:collection:update` / `shopify:search:update`
Both fire **before** the filtered results are fetched; the promise resolves with the result count once the grid has been updated. The `productFilters` entries follow the Storefront API `ProductFilter` format:
| Filter | Shape |
| ------------------- | ------------------------------------------------------------------------ |
| Availability | `{ available: true }` |
| Price range | `{ price: { min: 10, max: 50 } }` |
| Vendor / type / tag | `{ productVendor: "…" }`, `{ productType: "…" }`, `{ tag: "…" }` |
| Variant option | `{ variantOption: { name: "Color", value: "Blue" } }` |
| Metafield | `{ productMetafield: { namespace, key, value } }`(or `variantMetafield`) |
`sortKey` is the standard enum (`MANUAL`, `BEST_SELLING`, `TITLE`, `PRICE`, `CREATED` — searches only `RELEVANCE` or `PRICE`); it carries no direction, so ascending/descending collapse into one value.
```js
document.addEventListener("shopify:collection:update", (event) => {
console.log(event.collection.handle, event.productFilters, event.sortKey);
event.promise.then(({ productsCount }) => updateResultBar(productsCount));
});
```
`shopify:collection:update` fires before the new product grid is added to the DOM. If you need to reinitialize a widget after the grid has been updated, use the **`collection:rerendered`** theme event instead.
### Good to know
* **Don't use standard storefront events for analytics.** These events can fire regardless of the customer's tracking consent. Use [Web Pixels](https://shopify.dev/docs/apps/build/marketing-analytics/pixels) for analytics instead.
* **Development validation.** `shopify theme dev` serves a runtime that validates every payload and logs problems with a `[Shopify Standard Events]` console prefix; add `--standard-events-inspector` for a live event panel.
* The theme dispatches every event above from its own UI flows; cart changes made through `Shopify.actions.updateCart` are emitted by the runtime itself (`context: 'standard-action'`).
## Theme events
Motrix also provides theme-specific events for behaviors that aren't covered by Shopify's standard events.
These events use the same `document.addEventListener()` pattern.
| Event | Fires when | Payload |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| `quick-view:open` | Quick View modal/drawer opens | `detail.productUrl` |
| `quick-view:loaded` | Quick View content finishes loading (first open per product) | `detail.productUrl` |
| `variant:changed` | The selected variant's UI has been updated.
For reacting to the *selection itself*, prefer `shopify:product:select` — it fires earlier and resolves with the matched variant. | `detail.variant` (Shopify variant object) |
| `collection:rerendered` | The collection/search grid has been **re-rendered in the DOM** after a filter/sort change — the moment to re-initialize badges/widgets. (`shopify:collection:update` fires *before* the fetch; this fires *after*.) | — |
| `recommendations:loaded` | Product recommendations or recently viewed products finish loading | — |
| `page:loaded` | The page-transition overlay finishes revealing the page.
⚠️ Dispatched only when **page transitions** are enabled in theme settings, for a setting-independent hook use `DOMContentLoaded` or `shopify:page:view`. | — |
```js
document.addEventListener("collection:rerendered", () => {
reinitializeThirdPartyBadges();
});
document.addEventListener("quick-view:open", (event) => {
console.log(event.detail.productUrl);
});
```
### Legacy theme events
Still emitted for backward compatibility. For new customizations, use the standard Shopify APIs instead.
| Legacy event | Use instead |
| -------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `product-ajax:added` (`detail.product`) | `shopify:cart:lines-update` with `action: 'add'` — the resolved promise carries the cart |
| `product-ajax:error` (`detail.errorMessage`) | `userErrors` on the lines-update resolution, or `shopify:cart:error` for hard failures |
| `cart:updated` (`detail.cart`) | `shopify:cart:lines-update` / `Shopify.actions.getCart()` |