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
Collection is { id, handle, title, products: Product[] } — products is populated for collectionMulti and empty for collectionSingle.
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
basePricevsfinalPriceper row, andtotals.baseSubtotalvstotals.subtotalin the footer. - 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.
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.