Script&Theory

§25 / DEVELOPERS

REF: TG–DOC–25 · REV. 1.7

Event bus

Tangent dispatches three custom events with the standard 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

EventDispatched onBubbles`detail`Source
cart:updateddocumentNonone (null)assets/theme.js, cart.render()
cart:addedthe <product-form> elementYesnone (null)assets/theme.js, ProductForm
variant:changeddocumentNo{ 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.

example.js
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});
javascript

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.

example.js
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'
javascript

To go through the theme's request queue and section rendering in one step, use cart.request():

example.js
1await window.Tangent.cart.request(window.Tangent.routes.cartAdd + '.js', {
2 items: [{ id: 123456789, quantity: 1 }],
3});
javascript

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.

example.js
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});
javascript

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

FieldTypeDescription
variantIdstring | undefinedThe data-variant-id of the new product root
rootHTMLElementThe live [data-product-root] element after the update
example.ts
1interface VariantChangedDetail {
2 variantId: string | undefined;
3 root: HTMLElement;
4}
typescript

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.

example.js
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});
javascript

Shopify theme editor events

Shopify dispatches shopify:* events on document in the theme editor. Tangent handles these:

EventTangent handler
shopify:section:loadreveal(e.target) (editor only), loadModels(e.target) and syncThemeToggles()
shopify:section:selectOpens 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:deselectCloses <cart-drawer> and <menu-drawer> in the section. <promo-popup> and <age-gate> close.
shopify:block:selectScrolls 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:deselectCloses 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:

example.js
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}
javascript

Recipes

Meta Pixel on add to cart

example.js
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});
javascript

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.

example.js
1document.addEventListener('variant:changed', () => {
2 window.jdgm?.customizeBadges?.();
3 window.yotpo?.refreshWidgets?.();
4});
javascript
Get support ↗