Skip to content

Shopify Custom Pixels

Shopify’s Custom Pixels are sandboxed JavaScript environments that run inside a restricted iframe during checkout and on storefront pages. They’re Shopify’s answer to the tracking limitations created by Checkout Extensibility — a controlled way to run third-party JavaScript in the checkout while maintaining security and performance guarantees.

Understanding what Custom Pixels can and cannot do is the difference between a working checkout tracking implementation and hours of debugging.

Custom Pixels run in a web worker-like sandbox. You create them in Shopify Admin under Marketing > Customer Events. Each pixel is a JavaScript file that Shopify loads in a restricted iframe.

The sandbox has strict limitations:

  • No DOM access — no document.querySelector, no document.getElementById
  • No window.location — you cannot read the current URL directly
  • No direct cookie access — cookies are restricted
  • No localStorage or sessionStorage
  • Fetch is available — you can make network requests
  • analytics API is available — Shopify’s Customer Events subscription API

You can load GTM inside a Custom Pixel, but the restrictions affect what GTM can do.

// Custom Pixel script
// GTM loads in the sandbox
(function(w,d,s,l,i){
w[l]=w[l]||[];
w[l].push({'gtm.start': new Date().getTime(), event:'gtm.js'});
var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),
dl=l!='dataLayer'?'&l='+l:'';
j.async=true;
j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;
f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');

GTM triggers that work in Custom Pixels:

  • Custom Event triggers (the only reliable option)
  • Page View trigger (fires on pixel initialization)

GTM triggers that do NOT work:

  • Click triggers (no DOM access — no click delegation)
  • Form triggers (no DOM access)
  • Scroll triggers (no scroll events)
  • DOM Ready / Window Loaded (restricted environment)

The practical implication: if you load GTM in a Custom Pixel, build your entire checkout tracking on Custom Event triggers fired by the analytics.subscribe() API.

The analytics.subscribe() API is the correct way to receive Shopify checkout events inside a Custom Pixel.

// Subscribe to Shopify's ecommerce events
analytics.subscribe('checkout_completed', (event) => {
const checkout = event.data.checkout;
const items = checkout.lineItems.map((item, index) => ({
item_id: item.variant?.sku || item.variant?.id || item.id,
item_name: item.title,
item_variant: item.variant?.title !== 'Default Title' ? item.variant?.title : '',
price: parseFloat(item.variant?.price?.amount || 0),
quantity: item.quantity,
index: index,
}));
window.dataLayer = window.dataLayer || [];
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: checkout.order?.id || checkout.token,
value: parseFloat(checkout.totalPrice?.amount || 0),
tax: parseFloat(checkout.totalTax?.amount || 0),
shipping: parseFloat(checkout.shippingLine?.price?.amount || 0),
currency: checkout.currencyCode,
coupon: checkout.discountApplications?.[0]?.title || '',
items: items,
},
});
});

Shopify provides these standard events via the analytics.subscribe() API:

EventWhen it fires
page_viewedAny page view in the checkout flow
cart_viewedCart page view
checkout_startedCheckout initiation
checkout_address_info_submittedShipping address completed
checkout_shipping_info_submittedShipping method selected
checkout_contact_info_submittedContact info completed
payment_info_submittedPayment info entered
checkout_completedOrder confirmed
product_viewedProduct detail page view (storefront pixel only)
product_added_to_cartAdd to cart (storefront pixel only)
product_removed_from_cartRemove from cart (storefront pixel only)
// checkout_started → begin_checkout
analytics.subscribe('checkout_started', (event) => {
const checkout = event.data.checkout;
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'begin_checkout',
ecommerce: {
currency: checkout.currencyCode,
value: parseFloat(checkout.subtotalPrice?.amount || 0),
items: checkout.lineItems.map((item, i) => ({
item_id: item.variant?.sku || String(item.variant?.id),
item_name: item.title,
price: parseFloat(item.variant?.price?.amount || 0),
quantity: item.quantity,
index: i,
})),
},
});
});
// checkout_shipping_info_submitted → add_shipping_info
analytics.subscribe('checkout_shipping_info_submitted', (event) => {
const checkout = event.data.checkout;
const shippingLine = checkout.shippingLine;
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'add_shipping_info',
ecommerce: {
currency: checkout.currencyCode,
value: parseFloat(checkout.subtotalPrice?.amount || 0),
shipping_tier: shippingLine?.title || '',
items: checkout.lineItems.map((item, i) => ({
item_id: item.variant?.sku || String(item.variant?.id),
item_name: item.title,
price: parseFloat(item.variant?.price?.amount || 0),
quantity: item.quantity,
index: i,
})),
},
});
});

Custom Pixel vs. theme-based GTM vs. server-side

Section titled “Custom Pixel vs. theme-based GTM vs. server-side”
ApproachStorefrontCheckoutReliabilitySetup complexity
Theme-based GTM✅ Full❌ Non-Plus onlyHighLow
Custom Pixel (GTM inside)✅ Limited✅ Standard & PlusMediumMedium
Custom Pixel (no GTM)✅ Good✅ Standard & PlusHighLow-Medium
Server-side (sGTM)✅ Yes✅ YesVery HighHigh

For most standard Shopify merchants: use theme-based GTM for the storefront and Custom Pixels for checkout events.

GTM Preview Mode does not connect to GTM containers loaded inside Custom Pixel sandboxes. Use browser DevTools instead.

  1. Open Chrome DevTools (F12).

  2. In the Console tab, click the JavaScript context dropdown (it says “top” by default).

  3. Switch to the Custom Pixel iframe context — it appears as a Shopify subdomain or sandbox-* label.

  4. Now console.log(dataLayer) shows the pixel’s local dataLayer, and you can inspect events as they fire.

Add a temporary debug pixel to log all events:

// Temporary debug pixel — remove before going live
analytics.subscribe('all_events', (event) => {
console.log('[Pixel Debug]', event.name, JSON.stringify(event.data, null, 2));
});

See Shopify Custom Pixels: Debugging & Sandbox Guide for the complete debugging reference including postMessage workarounds and common error fixes.

Trying to use DOM-based GTM triggers in Custom Pixels. Click triggers, form triggers, and scroll triggers all fail silently. You won’t see errors — the triggers just never fire because GTM’s click delegation can’t attach to the DOM.

Not handling the Default Title variant. Shopify uses “Default Title” as the variant title for products with no variants. Always filter this out when constructing your items array.

Duplicating checkout_completed and order-status.liquid. If you have a Custom Pixel that tracks checkout_completed AND an order-status.liquid script that also tracks purchase, you’ll get duplicate purchase events. Implement one or the other, not both.