Events reference
En esta página
Poison announces every important storefront interaction as a DOM event, so apps, custom code and analytics can react without depending on the theme's markup. There are two layers:
-
Poison events (
cart:updated,variant:change, …):CustomEvents dispatched ondocumentwith adetailpayload. They always fire after the page has been updated, so listeners see the fresh state. -
Shopify standard storefront events (
shopify:cart:lines-update,shopify:page:view, …): the platform-wide events every app understands, dispatched from the element where the interaction happened. See Standard events.
This page is the reference for developers who build on the theme.
Listening
document.addEventListener('cart:updated', (event) => {
const { cart, action } = event.detail;
console.log(`Cart ${action}: ${cart.item_count} items`);
});
Every Poison event's detail also has timestamp (milliseconds) and source ("poison", or
"standard-action" when the change came from Shopify.actions.updateCart).
Asking the cart UI to refresh
After your code changes the cart outside the theme (for example with fetch('/cart/add.js')), tell
every cart UI on the page — drawer, header count, cart page, free-shipping goal — to re-render:
document.dispatchEvent(new CustomEvent('cart:refresh'));
Poison combines simultaneous refresh requests into one Section Rendering API request. Prefer
Shopify.actions.updateCart(...) when you can: it updates the cart and the theme's UI in one call and
emits the standard events for you.
Catalog
| Event | Fires when | detail |
|---|---|---|
cart:item-added |
Items were added successfully | { items, cart, sourceElement, productId, variantId, quantity } |
cart:item-removed |
A line was removed | { lineKey, item, cart } |
cart:item-updated |
A line's quantity changed | { lineKey, previousQuantity, quantity, cart } |
cart:updated |
Any cart change completed (after the specific event) | { cart, action } |
cart:refresh |
Fired by Poison after cart:updated with handled: true; listened for without it |
{ handled?, sections? } |
cart:error |
A cart request failed or the cart declined it | { action, message, status, variantId? } |
cart:note-updated |
The cart note was saved | { note, cart } |
cart:drawer-open / cart:drawer-close
|
The cart drawer opened (from the header button, or after cart:item-added when the cart type is drawer) or closed |
{ trigger } |
cart:empty |
The cart became empty | { cart } |
variant:change |
The selected variant changed and the product section updated | { productId, variant, previousVariant, sectionId, available } |
variant:unavailable |
The selected option values match no variant (a sold-out variant fires variant:change with available: false) |
{ productId, selectedOptions } |
product:quantity-change |
A product form's quantity changed | { productId, variantId, quantity } |
product:media-change |
The product gallery's active media changed | { productId, mediaId } |
quick-add:open / quick-add:close
|
The in-card options panel or quick add modal opened or closed | { productHandle } |
facets:updated |
A filter, sort or page change re-rendered a collection or the search results | { url, sectionId, resultsCount } |
search:predictive-results |
Predictive search results rendered | { term, resultCount } |
localization:change |
The country or language selector was submitted | { country, language } |
section:rendered |
Any section was re-rendered through the Section Rendering API: cart sections, a variant change, filters, predictive search, quick add; also when product recommendations, complementary products or recently viewed load |
{ sectionId }; cart sections add html, recommendations productId and intent
|
variant and previousVariant on variant:change are { id, exists, available, title, sku, price, currency, options, selected_options, featured_media_id } (price in the currency's subunit, as Liquid outputs it).
cart is the AJAX cart object (the shape returned by /cart.js). action on cart:updated is one
of add, remove, update, note or standard-action.
Example payloads
// cart:item-added
{
items: [{ id: 44011, variant_id: 44011, product_id: 8812, quantity: 1, title: 'Reflection slip dress - Lacquer red / S', … }],
cart: { token: '…', item_count: 3, total_price: 84900, items: [ … ], … },
sourceElement: <article class="po-card" …>,
productId: 8812,
variantId: 44011,
quantity: 1,
timestamp: 1791037200000,
source: 'poison'
}
// cart:item-updated
{ lineKey: '44011:8a6b…', previousQuantity: 1, quantity: 2, cart: { … } }
// cart:error
{ action: 'add', message: 'All 3 Reflection slip dress - Lacquer red / S are in your cart.', status: 422, variantId: 44011 }
// variant:change
{ productId: 8812, variant: { id: 44012, title: 'Ink / M', available: true, price: 42000, … }, previousVariant: { … }, sectionId: 'template--main', available: true }
// facets:updated
{ url: '/collections/new-in?filter.v.availability=1&sort_by=price-ascending', sectionId: 'template--main-collection', resultsCount: 24 }
Order and loop prevention
Every cart change follows the same order:
- The cart request is sent with the cart sections bundled (Section Rendering API).
- The returned HTML is swapped into the page.
- The specific event fires:
cart:item-added,cart:item-removed,cart:item-updatedorcart:note-updated. -
cart:updated { cart, action }. -
cart:empty { cart }, when the cart became empty. -
cart:refresh { handled: true, sections }.
Rule for cart UI: re-render on cart:refresh only when event.detail?.handled !== true.
Poison's own broadcast is already handled, so this never causes a second request:
document.addEventListener('cart:refresh', (event) => {
if (event.detail?.handled === true) return;
// fetch and re-render this component
});
An external cart:refresh (no detail) triggers one coalesced request for all cart sections; that
render emits section:rendered, never another cart:refresh.
Shopify standard events
Poison dispatches Shopify's standard storefront events for changes the theme makes itself, using
Shopify's library from https://cdn.shopify.com/storefront/standard-events.js:
| Standard event | Dispatched by Poison | From |
|---|---|---|
shopify:page:view |
Once per page, after DOMContentLoaded
|
document |
shopify:cart:lines-update |
Before every theme cart add, change or removal, with a promise that settles when the request finishes | the product card, product section or cart element |
shopify:cart:error |
When a cart request fails (not when the cart declines a change) | the same element |
shopify:cart:note-update |
As the cart note starts to save, with the note; its promise resolves with the cart | document |
shopify:product:view |
Once when the product page loads, and when the quick add modal shows a product (context: 'dialog'), from <po-view-event> (snippets/product-view-event.liquid): the product, its selected variant and options |
the product section |
shopify:collection:view |
Once when a collection page loads, from <po-view-event> in the product grid: the collection's ID, handle and product count |
the product grid section |
shopify:cart:view |
When the cart page loads and each time the cart drawer opens or re-renders while open (context: 'dialog'), from <po-view-event> (snippets/cart-view-event.liquid): the quantity, total, lines and discount codes |
the cart section |
shopify:product:select |
As a variant change starts, with the product and the chosen options; its promise resolves with the variant (ID, title, availableForSale, price, options), or null when no variant matches |
<po-product-info> (product page and featured product) |
shopify:collection:update, shopify:search:update
|
As a filter, sort or page change starts, with the filters and sort key read from the new URL; the promise resolves with the new number of results | <po-facets> |
When an app calls Shopify.actions.updateCart, the action emits the standard cart events itself;
Poison's configured handler then re-renders the cart sections and emits cart:updated, cart:empty
and cart:refresh { handled: true }, and returns detail.handledBy: 'poison' so app listeners can
skip their own re-render. Shopify.actions.openCart opens Poison's cart drawer, or the cart page
when the cart type isn't drawer.
For theme developers
Poison's modules are ES modules behind an import map:
import { POISON_EVENTS, emit, on } from '@poison/events';
import { add, change, update, get } from '@poison/cart-api';
const stop = on(POISON_EVENTS.CART_UPDATED, ({ detail }) => console.log(detail.cart));
await add({ items: [{ id: 44011, quantity: 1 }] });
stop();
emit(name, detail, target = document) adds timestamp and source. on(name, handler) returns a
function that removes the listener. Use the cart-api functions instead of calling the AJAX endpoints
directly: they bundle the cart sections and fire the events above in order.