defineRenderer exactly once, at the top level:
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:
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
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 — acollectionMulti 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.selectorsand branch onsel.kindbefore touching product data. - Single selectors: guard with
Array.isArray(entry)and treat an array asnull. Useentry.variantIdfor the selected<option>,entry.quantityfor the qty control. collectionMulti: for each product insel.collection.products, find its state by matchingentry.productId === product.idinside the array. Quantity0means unselected; on first load the array is empty, so every row shows0.- 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.baseSubtotalvstotals.subtotalin the footer, andbasePricevsfinalPriceper row — see the pricing rules above. - Derive currency from the variant’s
currencyCodeortotals.currencyCode. - Handle
collectionSinglewithresolvedProduct: 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.