Skip to main content
The JS asset must call defineRenderer exactly once, at the top level:
Do not wrap the call in DOMContentLoaded, setTimeout, or a conditional guard. Registration is matched to the loading bundle via document.currentScript, so a deferred call registers against nothing and the widget never renders.

The context object

There is no dispatch in the context — don’t look for one. ctx.t uses the English source string as the key, so there are no key names to invent:
A missing translation renders the literal you wrote, never a blank or a key. If you are editing a renderer that already ships ctx.t("…") literals, keep them byte-for-byte — the literal is the key to the merchant’s stored translations, and rewording it orphans every locale. Change the markup around a literal rather than the literal itself. Translate whole phrases, never fragments you concatenate, and avoid fixed-width buttons: translated copy runs up to 50% longer.

Hard rules

Escaping helper — apply to anything out of snapshot, never to ctx.t output (it escapes itself, and double-escaping shows shoppers "):

The snapshot

The snapshot also carries version: number (the bundle’s revision, used internally for cart stamping) — renderers have no reason to read it. All ids are Shopify GIDs. Collection is { id, handle, title, products: Product[] } — products is populated for collectionMulti and empty for collectionSingle.

Pricing rules

Prices are per unit on every entry, for every selector kind, and each entry carries its own — a collectionMulti row shows that product’s price, not the selector’s total. Multiply by entry.quantity yourself if you want a line total. Four rules beyond that: An entry with nothing applied — including a row the shopper hasn’t picked — reports its variant’s unit price with an empty appliedRewards. Above all: never recompute a discount yourself from conditionSets. Discount math is shared with the checkout-side function; re-deriving it in a renderer will disagree with what the shopper is charged.

Selector kinds

Reading recipes:
  • Iterate snapshot.selectors and branch on sel.kind before touching product data.
  • Single selectors: guard with Array.isArray(entry) and treat an array as null. Use entry.variantId for the selected <option>, entry.quantity for the qty control.
  • collectionMulti: for each product in sel.collection.products, find its state by matching entry.productId === product.id inside the array. Quantity 0 means unselected; on first load the array is empty, so every row shows 0.
  • Never hardcode a starting quantity. The engine sets initial quantities from the bundle’s first condition set before the first render — display entry.quantity.
  • Never fetch collection members yourself; they arrive in the snapshot.
  • Show discounts by comparing totals.baseSubtotal vs totals.subtotal in the footer, and basePrice vs finalPrice per row — see the pricing rules above.
  • Derive currency from the variant’s currencyCode or totals.currencyCode.
  • Handle collectionSingle with resolvedProduct: null — don’t invent a product.

PDP add-to-cart override

snapshot.meta.atcOverride is a merchant setting, not something the renderer controls. When it is true, the theme’s own add-to-cart button drives the bundle: the widget is mounted directly above it and the host intercepts the native click. Branch your footer on it: when atcOverride is true, omit your own ATC button entirely (the host strips stray [data-nameless-atc] nodes as a safety net). When it is false, render the button as usual. Trust the flag and nothing else. A bundle the merchant configured as a popup always arrives with atcOverride: false, because in that mode the theme button opens the popup and your button is the only way to add — the host rewrites the flag before your renderer sees it, so there is no popup case to special-case. See Shopify integration and Resolver stages.

When things go wrong

The host logs structured codes prefixed with [nameless]: Stay defensive: guard against empty selectors, null selections, empty collection.products, and missing image. If CSS fails, the JS is not executed, so treat both assets as required — but still write semantic, labelled markup so the widget survives a styling regression. Next: the data attributes the runtime wires after each paint.