Two events cover the whole shopper journey
Both are plain CustomEvents dispatched on document — attach your listeners to document (they don't bubble from anywhere else). No library, no App Bridge, no jQuery.
| Event | Fires | event.detail |
|---|---|---|
| hotspot-studio:product | Every time a product hotspot's popup opens (info and link hotspots don't fire it). | productGid, variantGid (when a variant is pinned), handle, showPriceRange, fbt — plus product: a Promise of the prefetched /products/<handle>.js JSON (resolves null if the fetch failed). |
| hotspot-studio:added | After every successful add to cart from any Hotspot Studio path — popup button, variant picker, Shop the Look, and the recommendations row — once the item is already in the cart server-side. | Empty by design — read /cart.js for the fresh cart state; it's already consistent with the new line. |
event.detail may carry additional private fields — only the ones listed above are stable.
document.addEventListener("hotspot-studio:product", function (e) {
const { productGid, variantGid, handle } = e.detail;
e.detail.product.then(function (product) {
if (!product) return; // fetch failed — product JSON unavailable
// product = /products/<handle>.js JSON — render reviews, badges, stock…
});
});
document.addEventListener("hotspot-studio:added", function () {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ event: "hotspot_add_to_cart" });
});
window.HotspotStudio() for AJAX themes
On a normal page load everything boots itself — sections hydrate lazily as they scroll near the viewport. The hook exists for markup that arrives later: quick-view modals, soft navigation, "load more".
- Idempotent — call it as often as you like; already-hydrated sections are skipped, only new [data-hs-root] elements are picked up.
- Still lazy — newly found sections hydrate when they approach the viewport, exactly like on first load.
- One rule when injecting markup — keep the block's inline <script type="application/json"> data tags intact; that's where the section's content lives.
- One prerequisite — the hook exists once the page has rendered at least one Hotspot Studio block (its Liquid loads the core; <script> tags injected via innerHTML never execute, so an injected section can't bootstrap it alone).
fetch(location.pathname + "?section_id=" + sectionId)
.then(function (r) { return r.text(); })
.then(function (html) {
container.innerHTML = html; // inject the freshly-rendered section
if (window.HotspotStudio) {
window.HotspotStudio(); // hydrate any new [data-hs-root] inside it
}
});
Work with our cart handling — or replace it entirely
One store-wide setting controls what happens after every add (popup, Shop the Look, recommendations, variant picker):
- Open the cart drawer (default) — drives the theme's own drawer automatically on Dawn-family and Horizon themes, with event and selector fallbacks for everything else.
- Advanced: drawer open selector — a Settings field (no code) that tells the fallback exactly which element toggles your drawer.
- Reload the page / Go to the cart page — the boring reliable options.
- Do nothing — added silently, and the app's cart script isn't even loaded. Pair it with hotspot-studio:added to take over completely:
document.addEventListener("hotspot-studio:added", function () {
fetch("/cart.js")
.then(function (r) { return r.json(); })
.then(function (cart) {
// update your UI from `cart`, then open your own drawer
});
});
How revenue lands on the right image
Every add made through the app tags the cart line with properties that power revenue attribution and section-scoped automatic discounts:
- _hs_section — on every line added through the app (the section's public id).
- _hs_hotspot — additionally on popup and variant-picker adds (the hotspot id).
- _hs_b — additionally on Shop-the-Look "add all" lines; the discount function uses it to apply the bundle deal instead of the per-product one.
- If you re-create cart lines yourself (custom cart logic, upsell apps), preserve every property with the _hs_ prefix — attribution and the discount function depend on them.
Built like you'd build it
Strict byte budgets
The core is capped at ~10 KB minified, zero dependencies. Companion scripts have their own hard caps and load deferred, only when enabled — per section for the variant picker, Shop the Look, recommendations and carousel; store-wide for cart handling (skipped entirely on "Do nothing").
Lazy by default
Sections hydrate via IntersectionObserver as they approach the viewport; product JSON is prefetched at hydration and cached per handle, so popups open with live prices instantly.
Real dialog semantics
Popups are role="dialog" with labels, Escape and outside-click close, focus moves in on open and restores on close, modals trap Tab, and prefers-reduced-motion is respected.
The fine print
This page is the whole stable surface: the two events, the hook, the _hs_-prefixed cart-line properties, and [data-hs-root]. Everything else — internal class names, the JSON inside the data tags, coordination events between our own scripts — is private and may change in any release. If you need something that isn't here, tell us instead of scraping the DOM; useful hooks get added.
Building something on top of it?
Agencies and theme developers get straight answers from the person who wrote the code — usually the same day.