Events
On this page
AndOr widgets dispatch DOM events when they mount, change and move between tiers. Upsell offers also publish Shopify customer events you can read in an optional custom pixel.
All DOM events are CustomEvents with bubbles: true. There is no
shadow root, so there is no composed flag.
Note The
andor:widget:*andandor:display:changeevents need the AndOr Discount Display app embed on. Blocks still show without it, but these events don’t fire.
Catalogue
These events are public. Names do not change, and payloads only ever
gain fields. The
three andor:offer_* events are Shopify customer events, not DOM
events: you read them in a custom pixel, not with addEventListener.
See Upsell offer pixel events.
| Event | Host | Detail type |
|---|---|---|
andor:widget:mount |
widget host | AndorWidgetEventDetail |
andor:widget:unmount |
host, document |
AndorWidgetEventDetail |
andor:display:change |
widget host | AndorDisplayChangeDetail |
andor:tier:change |
picker or table | AndorTierChangeDetail |
andor:goal:tier-change |
goal bar | AndorGoalTierDetail |
andor:offer_viewed |
customer events | AndorOfferPixelDetail |
andor:offer_clicked |
customer events | AndorOfferPixelDetail |
andor:offer_added |
customer events | AndorOfferPixelDetail |
When each one fires:
andor:widget:mount: the first pass that sees the host.andor:widget:unmount: the first pass after the host left the page. It is sent on the element and ondocument, because a removed node cannot bubble. A listener in one place sees exactly one.andor:display:change: a pass finds a state different from the stored one. Never on first paint.andor:tier:change: the volume picker’s checked card or the table’s current row moves to another tier. Not on first paint. A page with a table and a picker gets one event from each, told apart bydetail.source.andor:goal:tier-change: the goal bar’s count of reached steps changes after a cart change. Not on first paint.andor:offer_viewed: an upsell offer is first seen, once per offer and placement per visit.andor:offer_clicked: a shopper clicks an upsell offer.andor:offer_added: an upsell offer item was added to the cart.
How andor:display:change behaves
- No initial fire. The first pass sends
andor:widget:mountand stores the state silently. - Deduped. A pass with the same state sends nothing. A ticking countdown is silent: its time string is not part of the state.
- Up to one second late. The events script runs its own pass every
second, and on
visibilitychangeandpageshow. The page is right at the instant a sale ends. The event can arrive up to one second later. Treat it as a notification, not as the exact moment. - Variant switches show up as ordinary
andor:display:changeevents. A switch between two variants with the same deal and price sends nothing.
Note The events script only reports. If it never loads, prices, badges and countdowns still work.
Payloads
Show the code
type AndorWidgetType =
| "price"
| "badge"
| "countdown"
| "offer"
| "goal"
| "tiers"
| "bundle"
| "bxgy"
| "gift";
/** andor:widget:mount and andor:widget:unmount */
type AndorWidgetEventDetail = {
resource: {
widgetType: AndorWidgetType;
widgetId: string | null; // data-andor-widget-id, else null
productId: number | null; // data-andor-product-id, else null
};
};
type AndorDisplayState = {
discountId: string | null; // data-andor-discount-id, or the
// price block's data-best-id
kind: string | null; // data-andor-kind
mode: string | null; // data-andor-mode
visible: boolean; // !host.hidden
endsAt: string | null; // data-andor-ends, ISO 8601
priceCents: number | null; // always null today
originalCents: number | null; // data-price, integer cents
};
/** andor:display:change */
type AndorDisplayChangeDetail = {
resource: {
widgetType: AndorWidgetType;
widgetId: string | null;
productId: number | null;
previous: AndorDisplayState | null;
current: AndorDisplayState;
};
};
/** A tier: 0 based among the tiers, and its quantity threshold. */
type AndorTier = { index: number; quantity: number };
/**
* andor:tier:change. A tier is `null` below the first tier (the
* picker's 1 item card). Sent by the volume picker and the tiers
* table.
*/
type AndorTierChangeDetail = {
productId: number | null;
variantId: number | null;
previousTier: AndorTier | null;
currentTier: AndorTier | null;
/** Which widget sent it. One change, one event per widget. */
source: "picker" | "table";
/** The host's `data-andor-widget-id` (block id, or "pasted"). */
widgetId: string | null;
};
/**
* andor:goal:tier-change, from the goal bar. A tier is
* the count of steps reached: 0 none, 1 the first, and so on.
*/
type AndorGoalTierDetail = {
previousTier: number;
currentTier: number;
cartTotalCents: number; // the subtotal the bar measured
};
/** andor:offer_viewed, andor:offer_clicked, andor:offer_added */
type AndorOfferPixelDetail = {
offerId: string; // digits
placement: "product_page" | "cart_page" | "cart_drawer";
/** Adds only: the item's value, 0 for an auto-added gift. */
valueCents?: number;
/** A gift the storefront added on the shopper's behalf. */
auto?: true;
};The widget hosts that send these events, and their widgetType:
| Host element | widgetType |
|---|---|
<andor-price> |
price |
<andor-badge> |
badge |
<andor-countdown> |
countdown |
<li class="andor-offers__item"> |
offer |
<andor-goal> (the live bar) |
goal |
<andor-tiers> |
tiers |
<andor-bundle> (offer picker) |
bundle, bxgy or gift |
Only the live goal bar is a host. On the offers block, each row is a
host, not the <andor-offers> wrapper.
Tip New widget types can be added later. Treat an unknown
widgetTypeas a normal widget, not as an error.
Which fields each widget fills (the goal, tiers and offer picker hosts
read the same host attributes; a field the host does not carry is
null):
| Field | price | badge | countdown | offer |
|---|---|---|---|---|
widgetId |
block id | block id | block id | null |
productId |
id or null |
id or null |
id or null |
null |
discountId |
data-best-id |
id | id | id |
kind |
kind | kind | null |
kind |
mode |
"price" |
price or badge_only |
null |
same |
endsAt |
null |
ISO or null |
ISO | ISO or null |
originalCents |
cents | null |
null |
null |
Listening: examples
React to a sale ending:
document.addEventListener("andor:display:change", (event) => {
const { widgetType, current, previous } = event.detail.resource;
if (widgetType === "badge" && previous?.visible && !current.visible) {
console.log("this deal just ended:", previous.discountId);
}
});Track widgets across theme editor re-renders:
document.addEventListener("andor:widget:unmount", (event) => {
const { widgetType, widgetId } = event.detail.resource;
console.log("gone:", widgetType, widgetId);
});Follow the volume picker and the goal bar:
document.addEventListener("andor:tier:change", (event) => {
const { currentTier } = event.detail;
console.log(currentTier ? `tier ${currentTier.quantity}+` : "no tier");
});
document.addEventListener("andor:goal:tier-change", (event) => {
const { currentTier, cartTotalCents } = event.detail;
console.log("goal step", currentTier, "at", cartTotalCents);
});Upsell offer pixel events (optional)
AndOr needs no web pixel and installs none. Its own offer and analytics
counts go through the app proxy, so there is nothing to set up in
Customer events. The pixel below is optional. Add it only if you
want the andor:offer_* events in your own tools.
The three andor:offer_* events are not DOM events. They are published
to Shopify’s customer events with Shopify.analytics.publish, so a
custom pixel or an analytics app can read them. Subscribe in a custom
pixel:
// Settings > Customer events > Add custom pixel
analytics.subscribe("andor:offer_added", (event) => {
const { offerId, placement, valueCents } = event.customData;
console.log("upsell add", offerId, placement, valueCents);
});- A view is published once per offer and placement per visit, the same rule the app’s own counts use.
- No customer data, cart token or product ids are sent.
- The app’s own counting waits for the shopper’s analytics consent. Shopify’s customer events system applies its own consent rules to your pixel.
Events the widgets listen for
These are Shopify theme events. When a theme sends them, the widgets redraw with no extra code.
shopify:cart:lines-update: the goal bar, offer picker, volume picker line, goal fillers and loader re-read the cart.shopify:cart:view: the loader and the cart drawer goal load and draw the drawer widgets.shopify:section:load: the theme editor re-rendered a section. The goal bar, offers list, countdown, volume picker, offer picker and loader redraw it.shopify:product:select: a variant change (Horizon). The sale price follows the new variant.
The copy button in your own markup
One click listener on document copies any
.andor-code__button[data-discount-code] button. Your own code button
works with no extra JavaScript when it has this shape:
<span class="andor-code">
<button
type="button"
class="andor-code__button"
data-discount-code="{{ code | escape }}"
data-andor-copied="{{ 'discount_display.code_copied' | t }}"
aria-describedby="my-code-status"
>{{ code | escape }}</button>
<span class="andor-code__status andor-vh"
id="my-code-status" role="status"></span>
</span>It copies the code and swaps the button text to data-andor-copied
(default “Copied”). It fills the status element from
data-andor-copied-status, if set. Both are restored after 2 seconds.
The button must be a real <button> with the andor-code__button
class, holding only the code text.
See it in action