Reference
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:* and andor:display:change events 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 on document, 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 by detail.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:mount and 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 visibilitychange and pageshow. 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:change events. 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

TypeScript · 80 lines
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 widgetType as 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);
  }
});
JavaScript

Track widgets across theme editor re-renders:

document.addEventListener("andor:widget:unmount", (event) => {
  const { widgetType, widgetId } = event.detail.resource;
  console.log("gone:", widgetType, widgetId);
});
JavaScript

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);
});
JavaScript

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);
});
JavaScript
  • 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>
Liquid

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.

Next steps

Last updated

Was this page helpful?

No

Questions? Email andor@backtrackhq.com or use the chat in the app.