Events
The events Smootify sends when something happens on the page, how to listen with Smootify.on and once, which event to use for which task, and the ones sent on an element instead of the document.
On this page
Smootify sends an event each time something happens: the cart changes, a product loads, the visitor logs in or changes country. Your script listens and reacts. An event only tells what happened: what your handler returns is ignored. To change what Smootify does, use a filter. Each event, with its payload, is also in the Reference under API.
Listen to an event
window.Smootify = window.Smootify || [];
window.Smootify.push((Smootify) => {
const stop = Smootify.on("added_to_cart", ({ lines, cart }) => {
console.log(lines.length, cart.totalQuantity);
});
Smootify.once("initiate_checkout", ({ cart }) => console.log(cart.id));
// later: stop();
});- The name can be written with or without its
smootify:prefix:"cart_updated"or"smootify:cart_updated". onandoncereturn a function that stops listening.oncestops by itself after the first event.{ replay: true }, as the third argument ofon, also calls the handler at once with the last payload of an event that already happened.- Every event is also a DOM event, with the payload in
event.detail:document.addEventListener("smootify:cart_updated", (event) => console.log(event.detail)). Four of them are sent on an element instead ofdocument: see Events sent on an element. - Listening to
consent_changedis enough to make Smootify load Shopify’s consent script.
Add your listeners in a push function: they then get every event from the first product_loaded and cart_updated on. See Run your scripts with Smootify.
Which event for what
- Start your code once everything is loaded:
loaded, with{ replay: true }. - Keep your own cart counter or total in sync:
cart_updated. It also fires when the cart first loads. - Track an addition:
added_to_cart. For the checkout:initiate_checkout. - React to the chosen variant:
variant_changed. - Know whether the visitor is logged in:
user_auth_changewith{ replay: true }, orawait Smootify.getCustomer(). - Start analytics after consent:
consent_changed.
The events
Loading and products
Smootify.on("loaded",
() => { /* … */ })Smootify is loaded: the session, the market and the cart are known.
- Payload
- none
Smootify.on("product_loaded",
({ product, id }) => { /* … */ })A product loaded, once per product, after loaded.
product- the product, or null when Shopify does not return it: not published on the Headless channel, or not sold in the market
id- the id of the product asked for
Smootify.on("product_expired_load",
({ product, id }) => { /* … */ })A product shown from the cache has changed on Shopify.
product- the fresh product from Shopify
id- its id
Smootify.on("loadedAdditionalVariants",
({ product }) => { /* … */ })The page's product has more than 250 variants: it now carries all of them. Once, after its product_loaded.
product- the product with every variant
Smootify.on("variant_changed",
({ element, product, variant }) => { /* … */ })A product's variant changed, not at the first render.
element- the smootify-product on the page
product- its product
variant- the new variant, undefined for a combination that does not exist
document.querySelector("smootify-add-to-cart")
.addEventListener("smootify:changed_option",
(event) => { /* event.detail */ })
// detail: { optionChangedIndex, selectedValue, variant }The visitor picked an option value.
optionChangedIndex- the position of the option that changed
selectedValue- the value picked
variant- the variant it leads to, when it exists
variants- the product's variants
product- the product
In product_loaded, product is null when Shopify does not return the product: it is not published to the store’s sales channel, or not sold in the visitor’s market. In variant_changed, element is the smootify-product on the page and variant is undefined for a combination that does not exist.
Cart
Smootify.on("cart_updated",
cart => { /* … */ })Every change of the cart, the first load included.
cart- the whole cart: its lines, quantities, totals and discounts
Smootify.on("added_to_cart",
({ lines, cart, goToCheckout }) => { /* … */ })Lines were added, after Shopify answered.
lines- the lines added: the variant (merchandiseId), the quantity, the line attributes and the selling plan
cart- the cart after the addition
goToCheckout- true with Buy now
Smootify.on("removed_from_cart",
lineIds => { /* … */ })Lines were removed.
lineIds- the ids of the cart lines removed
Smootify.on("errors",
errors => { /* … */ })Shopify refused a cart operation, or a filter threw an error.
errors- each error with its message, and the field and code when Shopify gives them
Smootify.on("initiate_checkout",
({ cart }) => { /* … */ })The visitor leaves for checkout.
cart- the cart the visitor checks out
Smootify.on("cart_opened",
({ element }) => { /* … */ })A mini cart opened. cart_closed is sent when it closes.
element- the smootify-cart that opened or closed
cart_opened and cart_closed fire for every kind of mini cart (dialog, details, Webflow Dropdown, popover or your own trigger), never for a cart page. element is the smootify-cart.
Customer, market and consent
Smootify.on("user_auth_change",
customer => { /* … */ })The session is known, or changes.
customer- the logged-in customer, or null for a guest
Smootify.on("changed_country",
({ country, oldCountry }) => { /* … */ })The visitor's country changed.
country- the new country code, such as IT
oldCountry- the country before
Smootify.on("consent_changed",
consent => { /* … */ })The consent is known on the page, then at every change.
consent- loaded (Shopify's script answered), required (the banner shows), allowed (true or false per category), answers, region, saleOfDataRegion and interacted
Smootify.on("created_address",
({ address }) => { /* … */ })A create-address form saved an address.
address- the address saved
The first user_auth_change goes out before the push functions run. Listen with { replay: true } to get it.
Wishlist
Smootify.on("wishlist:changed_product_state",
({ node, product, active }) => { /* … */ })The visitor added (active: true) or removed a product.
node- the wishlist-toggle
product- the product added or removed
active- true when added, false when removed
Smootify.on("wishlist:changed_product",
({ node, product }) => { /* … */ })A wishlist-toggle got its product.
node- the wishlist-toggle
product- the product it now follows
Smootify.on("wishlist:changed_variant_state",
({ node, variant, active }) => { /* … */ })The visitor added or removed a variant.
node- the wishlist-toggle
variant- the variant added or removed
active- true when added, false when removed
Smootify.on("wishlist:changed_variant",
({ node, variant }) => { /* … */ })The toggle follows another variant.
node- the wishlist-toggle
variant- the variant it now follows
node is the wishlist-toggle. More in Wishlist.
Search and discovery
document.querySelector("smootify-search-discovery")
.addEventListener("smootify:filter_initial_change",
(event) => { /* event.detail */ })
// detail: { name, value }A filter was applied from selected-value or the page URL, before the first search.
name- the label of the filter
value- the value applied, or the list of values; undefined when none
document.querySelector("smootify-search-discovery")
.addEventListener("smootify:filter_changed",
(event) => { /* event.detail */ })
// detail: { name, value }The visitor changed a filter (name: "query" for the search text).
name- the label of the filter, or query for the search text
value- the value chosen, or the list of values
document.querySelector("smootify-search-discovery")
.addEventListener("smootify:sort_changed",
(event) => { /* event.detail */ })
// detail: { name: "sort", value }The visitor changed the sorting.
name- always sort
value- the sort option chosen
These three are sent on an element: see below.
Events sent on an element
Four events are not sent on document, and do not bubble up to it:
changed_optionis sent on thesmootify-add-to-cartform around the picker, or on the picker when it is outside the form;filter_initial_change,filter_changedandsort_changedare sent on thesmootify-search-discoveryblock, or on thesmootify-search-pageblock.
Where you listen decides whether your handler runs:
On document
Not called. The event is sent on the element and does not bubble up to document. The same for a plain document listener.
On document, in the capture phase
Called. With true as the third argument, the listener catches the event on its way down to the element.
On the element
Called. The event is sent there: listen on the element named in the list above.
The two ways that work, in code:
document.addEventListener(
"smootify:changed_option",
(event) => console.log(event.detail.optionChangedIndex, event.detail.selectedValue),
true,
);
document
.querySelector("smootify-search-discovery")
?.addEventListener("smootify:filter_changed", (event) => console.log(event.detail.name, event.detail.value));To react to a new variant, variant_changed is simpler: it is sent on document and carries the product element.
Other DOM events
Smootify also sends a few DOM events kept from 1.x. They are not in smootify.d.ts:
productLoadedandchangeVarianton eachsmootify-productelement, with the product or the variant inevent.detail. They do not bubble: listen on the element.changeVariantalso fires at the first render.changePlanon eachsmootify-productelement, when the visitor picks a selling plan or goes back to the one-time purchase.event.detailhasplan(nullfor the one-time purchase) andvariant. It does not bubble: listen on the element.addon:state_changeondocumentand on thesmootify-add-to-cartform, each time an add-on is picked or dropped.event.detailhaswrapper(the add-on element),product,variantandincluded.variantsRenderedondocument.body, each time a variant card (smootify-variant) is filled.smootify:productImagesModuleLoadedondocument, each time a deprecatedproduct="images"element draws its images. Theproduct="gallery"element does not send it. It does not mean every image has finished downloading.smootify:magic_box_changeondocument.body, once the box has loaded and after each change: see Example: a Magic Box title that follows its contents.smootify:price_renderondocument, each time asmootify-pricewrites a new amount.event.detail.elementis the price element.smootify.devMetafieldandsmootify.renderedMetafieldsondocument, for metafields: see Render a metafield yourself.
Example: a Magic Box title that follows its contents
smootify:magic_box_change carries element (the smootify-magic-box), currentQuantity (how many products the box holds) and cartItems, one { lineItem, variant } per product. The box element has two methods: modifyBoxTitle(title) and modifyBoxImage(url), which accepts only an image served by Shopify’s CDN (upload it under Content > Files). The new title and image are the ones the box line takes in the cart and at checkout.
document.body.addEventListener("smootify:magic_box_change", (event) => {
const { element, currentQuantity } = event.detail;
element.modifyBoxTitle(`Box of ${currentQuantity} wines`);
element.modifyBoxImage(
currentQuantity > 3 ? "https://cdn.shopify.com/…/box-large.jpg" : "https://cdn.shopify.com/…/box-small.jpg",
);
});Add the listener before the box loads, in the page’s head code, so it also gets the first event.
Example: the store credit a cart earns
When a Shopify Flow workflow gives store credit on paid orders, like Store credit on a paid order (5% of the subtotal from 50 up), the cart can say what this order will earn. Smootify has no attribute for it, because the rule is yours: the percentage, the minimum and whether to show it at all. A few lines on cart_updated do it.
Put the message next to the cart’s subtotal, outside the line template, and hide it in Webflow:
<p data-credit-estimate hidden>This order earns you <span data-credit-amount></span> of store credit.</p>Then add the script, with the same numbers as your workflow:
window.Smootify = window.Smootify || [];
window.Smootify.push((Smootify) => {
const RATE = 0.05; // the share your workflow credits
const MINIMUM = 50; // its minimum subtotal
const SHOP_CURRENCY = "EUR"; // the currency of that minimum
Smootify.on("cart_updated", (cart) => {
const { amount, currencyCode } = cart.cost.subtotalAmount;
const subtotal = Number(amount);
const earns = currencyCode === SHOP_CURRENCY && subtotal >= MINIMUM;
document.querySelectorAll("[data-credit-estimate]").forEach((message) => {
message.hidden = !earns;
const value = message.querySelector("[data-credit-amount]");
if (earns && value) value.textContent = Smootify.formatMoney((subtotal * RATE).toFixed(2));
});
}, { replay: true });
});- It is an estimate. The credit is what the workflow gives when the order is paid. Change the numbers in both places, or the cart promises what the workflow does not give.
- One currency. The minimum is in the shop’s currency, so the message shows only when the visitor’s cart is in that currency. In other markets it stays hidden.
{ replay: true }shows it at once for the cart that already loaded.
Where people get stuck
Check these first
Smootify.on("changed_option")never runs. The event is sent on the form or the picker, not ondocument. Listen in the capture phase, or usevariant_changed.Smootify.on("filter_changed")never runs. The same: the event is sent on the search and discovery block. Listen on that element.- The first
user_auth_changeis missed. It goes out before thepushfunctions run. Add{ replay: true }. - A script added by a tag manager never hears
loaded. The event fired before the script arrived. Add{ replay: true }. - Returning a value from a handler changes nothing. Events only report. To change the lines, the checkout address or a draft order, use a filter.
- A wishlist handler runs twice. It listens to both the 1.x name (
smootify_wishlist:…) and the 2.0 name, and Smootify sends both. Keep the 2.0 name.
Coming from 1.x
The names and the payloads of the events are the same as in 1.x, with these changes:
- Wishlist events are renamed
smootify:wishlist:…, like every other event. The 1.x names (smootify_wishlist:changed_product,changed_product_state,changed_variant,changed_variant_state) are still sent, with the same payload, until 3.0. - New in 2.0:
cart_opened,cart_closed,consent_changedandvariant_changed. - Now sent:
created_addressand the twowishlist:changed_variantevents, which 1.x declared and never sent. - One event per thing that happened.
product_loadedgoes out once per product,changed_optiononly when the visitor picks a value,sort_changednot for aselected-valuepreselection, andsmootify:price_renderonly when the amount changes. A script that counted events to detect the end of loading should listen toloaded. smootify:color_patternsis no longer sent: 2.0 does not load the shop’s colour patterns.Smootify.onandonceare new: in 1.x, scripts useddocument.addEventListener, which still works.