# 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()` |