> 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/developer-platform/events.md).

# Events

How to listen for Shopify storefront events and Analog theme events, including payloads and commands.

Analog reports variant selection, cart updates, and collection filtering through Shopify's [standard storefront events](https://shopify.dev/docs/api/storefront-events-and-actions), with names prefixed `shopify:`. Drawers, quickview, section visibility, and videos use Analog's `theme:` events because Shopify doesn't define standard events for them. Custom code can listen for both and dispatch the documented `theme:` commands.

## Event payloads

Events bubble to `window`. Use `event.target` to find the source element, or add one listener on `window` for the whole page. The prefixes use different payloads:

* **`shopify:` events put standard values on the event itself**, such as `event.action`, `event.lines`, and `event.product`. Theme-specific values are in `event.detail`. Commerce events that wrap a request also carry `event.promise`: the event fires when the action starts, and the promise resolves when the result is in.
* **`theme:` events are `CustomEvent`s** with their payload on `event.detail`.

Code written for Shopify's standard `shopify:` events can work across themes that implement the same events.

## Listening

In [custom code](/developer-platform/custom-code.md), use plain `addEventListener` in `custom.events.js`:

```js
window.addEventListener('shopify:cart:lines-update', async (event) => {
  console.log(event.action, event.lines);

  const {cart, detail} = await event.promise;
  console.log(cart?.totalQuantity, detail?.didError);
});
```

In Liquid markup or a [Custom liquid block](/developer-platform/blocks.md), use an Alpine listener with the `.window` modifier since the event may not bubble through your element:

```html
<div x-data @shopify:product:select.window="console.log($event.detail.variantId)">
  …
</div>
```

## Check the section id

A page can contain the main product section, quickview, and several Featured product sections. Each dispatches product events with `sectionId` in `event.detail`. Check it when the listener should handle only one product form:

```js
window.addEventListener('shopify:product:select', (event) => {
  const sourceSection = event.target.closest('.shopify-section');
  const sourceSectionId = sourceSection?.id?.replace('shopify-section-', '');
  if (event.detail.sectionId !== sourceSectionId) return;

  // The event came from the section you expect.
});
```

In Liquid the check is one comparison, because `section.id` is available at render time:

```html
@shopify:product:select.window="if ($event.detail.sectionId !== '{{ section.id }}') return; …"
```

## Product events

| Event                    | Fires when                                           | Payload                                                                                                                            |
| ------------------------ | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `shopify:product:select` | A shopper picks a variant option in a product form   | `event.product` `{id, title, handle}`, `event.selectedOptions` `[{name, value}]`, `event.promise` resolving to `{variant, detail}` |
| `theme:sibling:change`   | A shopper picks a sibling product (grouped variants) | `event.detail` `{siblingId, productId, sectionId, url, handle}`                                                                    |

Analog adds `{variantId, productId, sectionId, variantFeaturedMediaId}` to `shopify:product:select`'s `detail`. Use these values for section checks and media updates. Variant data renders with the page, so the promise resolves immediately.

Both events bubble from the selected option control. Use `shopify:product:select` for custom swatches, preorder logic, or other code that tracks the selected variant. Shopify has no standard event for changing to a different sibling product, so that action uses `theme:sibling:change`. See [siblings & grouped variants](/features/variants.md).

## Cart events

| Event                       | Fires when                                                                 | Payload                                                                                         |
| --------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `shopify:cart:lines-update` | A product form adds an item, or a cart line changes quantity or is removed | `event.action` `'add' \| 'update' \| 'remove'`, `event.context`, `event.lines`, `event.promise` |
| `shopify:cart:note-update`  | A cart note form submits with a changed note                               | `event.note`, `event.context`, `event.promise`                                                  |
| `shopify:cart:error`        | A cart request failed                                                      | `event.error` (message), `event.code`, `event.detail` `{description, errors}`                   |

The lines-update and note-update events fire **before** their request starts, and `event.promise` resolves once it finishes, with `{cart, detail}`:

* `cart`: the cart summary after the change: `{id, totalQuantity, cost, lines, discountCodes}`, or `null` when the request failed.
* `detail`: the theme's extras: `sections` (section-rendering HTML keyed by section id), `items` (the full cart's items), `item` (the added line with its `url`, add only), `itemCount`, `source` and `sourceId` (which form dispatched, so a component can skip its own events), and `didError`.

A failed request still resolves the promise with `detail.didError: true`, so a listener that awaits the promise always settles. `shopify:cart:error` also fires. If a newer request replaces the current one, the cancelled request resolves with `detail.aborted: true` and doesn't fire an error event. Add events come from the product form ([`x-product-form`](/reference/alpine/product-form.md)), line changes from [`x-cart-item`](/reference/alpine/cart-item.md), and note updates from [`x-cart-form`](/reference/alpine/cart-form.md). All bubble from the form element to `window`.

Analog listens for `shopify:cart:lines-update` and follows the "Add to cart action" setting by opening the drawer, showing a notification, or navigating to the Cart page. Cart events have no `sectionId` because the cart is global.

## View events

| Event                     | Fires when                                                                                                                                                                                        |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `shopify:page:view`       | Once on every page load, with `event.page` `{template, title, url}`                                                                                                                               |
| `shopify:product:view`    | The main product renders, a product card scrolls half into view, or quickview opens. `event.context` identifies the source (`'page'`, `'collection'`, `'search'`, `'recommendation'`, `'dialog'`) |
| `shopify:collection:view` | A collection page renders, with `event.collection`                                                                                                                                                |
| `shopify:cart:view`       | The cart page renders or the cart drawer opens, with `event.cart`                                                                                                                                 |

View payloads come from Shopify's `standard_event_data` Liquid filter, rendered into `data-view-event-payload` attributes and dispatched by the theme's `x-view-event` directive.

`shopify:cart:view` fires after the Cart drawer content loads. This matters when an app opens the drawer before its cart update arrives: the event contains the cart shown in the drawer. Reopening the drawer fires another event. Refreshing the content while it stays open doesn't.

## Collection and search events

| Event                       | Fires when                                                                 | Payload                                                                                                    |
| --------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `shopify:collection:update` | A collection page's products change from filtering, sorting, or pagination | `event.collection` `{id, handle, productsCount}`, `event.productFilters`, `event.sortKey`, `event.promise` |
| `shopify:search:update`     | A search results page updates, or predictive search renders suggestions    | `event.search` `{query, productFilters, sortKey}`, `event.promise` resolving to `{totalCount}`             |

Analog dispatches these after the new results render, so the promise resolves immediately.

## Actions

Shopify adds a `Shopify.actions` object to the page. Use it to update and open the cart without depending on Analog's cart markup:

```js
// Add a variant and let the theme's drawer, icon, and sections react
await Shopify.actions.updateCart({lines: [{merchandiseId: 12345, quantity: 1}]});

// Open the cart the way the merchant configured it: drawer or cart page
await Shopify.actions.openCart();

// Read the current cart
const {cart} = await Shopify.actions.getCart();
```

`updateCart` sends the request and dispatches the same `shopify:cart:*` events as the theme forms. Cart sections update from those events. Use it instead of calling `/cart/add.js` directly. See the [actions reference](https://shopify.dev/docs/api/storefront-events-and-actions/actions).

## Section events

Section API events fire on `window` when Analog fetches or replaces section HTML. This includes variant changes, collection filters, and drawer loads. All four include `{sectionId, url}` in `event.detail`.

| Event                    | Fires when                                             |
| ------------------------ | ------------------------------------------------------ |
| `theme:section:load`     | A section fetch starts                                 |
| `theme:section:loaded`   | The section HTML arrived                               |
| `theme:section:update`   | The section's HTML on the page was replaced            |
| `theme:section:navigate` | A section navigation finished and the page URL changed |

Two more report visibility. They bubble from the section element itself:

| Event                 | Fires when                       | `detail`      |
| --------------------- | -------------------------------- | ------------- |
| `theme:section:enter` | The section entered the viewport | `{sectionId}` |
| `theme:section:leave` | The section left the viewport    | `{sectionId}` |

While scrolling, a section enters when it reaches the middle of the viewport or becomes fully visible. When scrolling stops, any partly visible section counts as entered. Use these events to start and stop work that only matters on screen.

Section fetching is driven by [`x-section-api`](/reference/alpine/section-api.md).

## Scroll events

The page dispatches `theme:scroll:down` and `theme:scroll:up` on `document` as the page scrolls in that direction, with the scroll position in `detail`:

```js
document.addEventListener('theme:scroll:down', (event) => {
  console.log(event.detail.position);
});
```

## Events you dispatch

Dispatch these events on `window` to control Analog. Built-in controls use the same events.

| Event                                     | `detail`                                   | What the theme does                               |
| ----------------------------------------- | ------------------------------------------ | ------------------------------------------------- |
| `theme:cart-drawer:open`                  | none                                       | Opens the cart drawer                             |
| `theme:cart-drawer:close`                 | none                                       | Closes the cart drawer if it is open              |
| `theme:menu-drawer:open`                  | none                                       | Opens the mobile menu drawer                      |
| `theme:menu-drawer:close`                 | none                                       | Closes the menu drawer if it is open              |
| `theme:quickview:open`                    | `{url}`, a required product URL            | Opens the quickview drawer and loads that product |
| `theme:quickview:close`                   | none                                       | Closes the quickview if it is open                |
| `theme:quickview:preload`                 | `{url}`                                    | Fetches quickview content ahead of opening        |
| `theme:pickup:open`                       | `{url}`, a variant URL                     | Opens the pickup availability drawer              |
| `theme:pickup:close`                      | none                                       | Closes the pickup drawer if it is open            |
| `theme:pickup:preload`                    | `{url}`                                    | Fetches pickup content ahead of opening           |
| `theme:video:play` / `theme:video:pause`  | a `<video>` element id, or an array of ids | Plays or pauses the matching theme videos         |
| `theme:video:mute` / `theme:video:unmute` | a `<video>` element id, or an array of ids | Mutes or unmutes the matching theme videos        |

```js
window.dispatchEvent(
  new CustomEvent('theme:quickview:open', {detail: {url: '/products/example-product'}}),
);
```

Quickview and pickup buttons dispatch their preload events on hover and focus. Use `Shopify.actions.updateCart` for cart changes instead of dispatching an event.

## Migrating from the theme:\* commerce events

Release 1.1 replaced the old commerce events with Shopify's standard events. The old names don't fire. The table maps each old name to its replacement. Pipeline includes these mappings in an optional `assets/events.shim.js`. Its listeners are plain JavaScript and can be copied into `custom.events.js`.

| Old event                              | New event                                                                                    |
| -------------------------------------- | -------------------------------------------------------------------------------------------- |
| `theme:variant:change`                 | `shopify:product:select`. Theme values (`variantId`, `sectionId`, …) moved to `event.detail` |
| `theme:cart:add:success`               | `shopify:cart:lines-update` with `event.action === 'add'`, result via `await event.promise`  |
| `theme:cart:add:error`                 | `shopify:cart:error`                                                                         |
| `theme:cart:change:success` / `:error` | `shopify:cart:lines-update` with action `'update'` or `'remove'` / `shopify:cart:error`      |
| `theme:cart:update:success` / `:error` | `shopify:cart:note-update` or `shopify:cart:lines-update` / `shopify:cart:error`             |
| `theme:collection:update`              | `shopify:collection:update`                                                                  |

## Internal media events

Analog also uses these internal media events: `theme:video:init`, `theme:video:playing`, `theme:video:paused`, `theme:video:muted`, `theme:video:unmuted` (each with the video element's id as detail), `theme:youtube:loaded`, `theme:vimeo:loaded`, `theme:model:loaded`, `theme:models:loaded`, and `theme:cart-notification:show`. Don't use them in custom code because they can change between versions.

## See also

* [Custom code](/developer-platform/custom-code.md): where to load `custom.events.js`
* [Developer blocks](/developer-platform/blocks.md): Custom liquid blocks with Alpine listeners
* [Siblings & grouped variants](/features/variants.md): `theme:sibling:change`
* [Cart & conversion](/features/cart.md): cart drawer, notifications, and Cart page
