§25 / DEVELOPERS
REF: TG–DOC–25 · REV. 1.7
Event bus
CustomEvent constructor. Your scripts can listen for them on document. There is no separate event bus object and no tangent:-prefixed events.TANGENT 1.7 / SHOPIFY OS 2.0 / UPDATED 2026-10-19
| Event | Dispatched on | Bubbles | `detail` | Source |
|---|---|---|---|---|
cart:updated | document | No | none (null) | assets/theme.js, cart.render() |
cart:added | the <product-form> element | Yes | none (null) | assets/theme.js, ProductForm |
variant:changed | document | No | { variantId, root } | assets/product.js, VariantPicker |
cart:updated
Dispatched on document at the end of window.Tangent.cart.render(). This runs after every cart request made through the theme (add, change, gift wrap) and after cart.refresh(). By then the cart sections are already replaced in the DOM and the update has been announced to screen readers.
The event has no detail. Read the cart state from the DOM or the Cart API instead.
1document.addEventListener('cart:updated', () => {2 const count = window.Tangent.cart.count();3 window.dataLayer = window.dataLayer || [];4 window.dataLayer.push({ event: 'cart_updated', cartCount: count });5});Updating the theme after your own Cart API call
If your script changes the cart directly (for example POST /cart/add.js), call cart.refresh(). It fetches the cart sections with GET /cart?sections=…, re-renders them, and dispatches cart:updated.
1await fetch(window.Tangent.routes.cartAdd + '.js', {2 method: 'POST',3 headers: { 'Content-Type': 'application/json', Accept: 'application/json' },4 body: JSON.stringify({ items: [{ id: 123456789, quantity: 1 }] }),5});6await window.Tangent.cart.refresh(); // dispatches 'cart:updated'To go through the theme's request queue and section rendering in one step, use cart.request():
1await window.Tangent.cart.request(window.Tangent.routes.cartAdd + '.js', {2 items: [{ id: 123456789, quantity: 1 }],3});cart:added
Dispatched by <product-form> after its add-to-cart request succeeds, before the drawer opens or the "added" toast shows. It bubbles, so you can listen on document.
The event has no detail. Use event.target (the <product-form>) to read what was submitted.
1document.addEventListener('cart:added', (event) => {2 const form = event.target.querySelector('form');3 const variantId = form?.querySelector('[name="id"]')?.value;4 const quantity = form?.querySelector('[name="quantity"]')?.value || 1;5 console.log(`Added ${quantity} of variant ${variantId}`);6});Inside the theme, <quick-order> listens for cart:added on itself to reset its quantities.
cart:added is only dispatched for AJAX adds. When Cart type is Page and the form has no data-always-ajax attribute, the form posts normally and no event fires.
variant:changed
Dispatched on document by <variant-picker> after the new variant's section HTML has been swapped in, the URL updated, and the price and button text announced.
Payload
| Field | Type | Description |
|---|---|---|
variantId | string | undefined | The data-variant-id of the new product root |
root | HTMLElement | The live [data-product-root] element after the update |
1interface VariantChangedDetail {2 variantId: string | undefined;3 root: HTMLElement;4}root.dataset.featuredMediaId holds the featured media id of the new variant.
Inside the theme, <media-gallery> and <sticky-buy> listen for this event and ignore it unless root contains them.
1document.addEventListener('variant:changed', (event) => {2 const { variantId, root } = event.detail;3 const price = root.querySelector('.price')?.textContent.trim();4 const submit = root.querySelector('[data-buy-buttons] [type="submit"]');5 console.log(`Variant ${variantId}: ${price}, available: ${!submit?.disabled}`);6});Shopify theme editor events
Shopify dispatches shopify:* events on document in the theme editor. Tangent handles these:
| Event | Tangent handler |
|---|---|
shopify:section:load | reveal(e.target) (editor only), loadModels(e.target) and syncThemeToggles() |
shopify:section:select | Opens a <cart-drawer> in the section, or a <menu-drawer> below 990 px wide. <promo-popup> and <age-gate> open when their section is selected. |
shopify:section:deselect | Closes <cart-drawer> and <menu-drawer> in the section. <promo-popup> and <age-gate> close. |
shopify:block:select | Scrolls a parent .slider to the block, activates a showcase menu item, and opens a closed <details> (marked data-editor-opened). <slide-show> goes to the selected slide; <style-quiz> shows the selected question. |
shopify:block:deselect | Closes a <details> that was opened with data-editor-opened |
shopify:section:unload and shopify:section:reorder are not handled. Elements clean up their own listeners in disconnectedCallback.
The reveal, block:select, block:deselect, section:select and section:deselect handlers in theme.js are bound only when window.Shopify?.designMode is true:
1// assets/theme.js (excerpt)2if (window.Shopify?.designMode) {3 document.addEventListener('shopify:section:load', (e) => reveal(e.target));4 document.addEventListener('shopify:block:select', (e) => {5 const slider = e.target.closest('.slider');6 if (slider) {7 const delta = e.target.getBoundingClientRect().left - slider.getBoundingClientRect().left;8 slider.scrollBy({ left: delta, behavior: 'smooth' });9 }10 activateShowcase(e.target.closest('.showcase__item'));11 const details = e.target.closest('details') || e.target.querySelector?.(':scope > details');12 if (details && !details.open) {13 details.open = true;14 details.dataset.editorOpened = '';15 }16 });17 document.addEventListener('shopify:block:deselect', (e) => {18 const details = e.target.closest('details[data-editor-opened]');19 if (details) {20 details.open = false;21 delete details.dataset.editorOpened;22 }23 });24}Recipes
Meta Pixel on add to cart
1document.addEventListener('cart:added', (event) => {2 if (typeof fbq !== 'function') return;3 const variantId = event.target.querySelector('form [name="id"]')?.value;4 fbq('track', 'AddToCart', {5 content_ids: [variantId],6 content_type: 'product',7 currency: window.Shopify?.currency?.active,8 });9});Re-initializing a review widget after a variant change
variant:changed replaces parts of the product section. If a review app renders into that markup, ask it to re-scan. The method names below belong to the third-party apps, not to Tangent; check your app's documentation.
1document.addEventListener('variant:changed', () => {2 window.jdgm?.customizeBadges?.();3 window.yotpo?.refreshWidgets?.();4});Last updated for Tangent 1.7