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

All ids are Shopify GIDs. Collection is { id, handle, title, products: Product[] }products is populated for collectionMulti and empty for collectionSingle.

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 basePrice vs finalPrice per row, and totals.baseSubtotal vs totals.subtotal in the footer.
  • 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. 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.