Browse articles

Custom HTML for developers

Write Custom HTML scripts that work with Shadow DOM, render at the right time and talk to the cart through the EliteCart API.

Last updated

This article is for whoever writes or reviews the scripts in Custom HTML: how the cart renders your code, how to find your elements with Shadow DOM on, and which EliteCart API calls to use. Follow it and the same snippet works on every store, on an empty cart and after the cart is emptied and refilled.

The AI generator in Custom HTML already follows these rules. Read on when you write code by hand, paste code from another app, or check what the AI wrote.

Find your elements: eliteCartDocumentOrShadowRoot

window.eliteCartDocumentOrShadowRoot points to the cart's shadow root when Shadow DOM is on, and to document when it's off. Code that uses it works in both modes:

var root = window.eliteCartDocumentOrShadowRoot || document;
var banner = root.querySelector("#my-banner");

Use querySelector("#id") rather than getElementById. Keep using plain document only for elements outside the cart, such as your theme's header or a theme modal.

With Shadow DOM on, your theme's CSS doesn't reach the cart either. Style your markup with the tw- classes the editor suggests (tw-text-primary and tw-bg-secondary follow your cart colors), inline styles, or a <style> block inside the snippet. Custom CSS also still applies.

When your code renders and runs

PositionRendered on an empty cart?
Top of cart & general scriptsYes
Below headerYes
Above line items, Below line items, Top of cart summary, Above checkout button, Below checkout button, Bottom of cartNo, only when the cart has items

What this means for scripts:

  • The cart renders when the page loads, while the drawer is still closed. Scripts in Top of cart & general scripts and Below header run once per page load.
  • The other six positions mount only when the cart has items. If the cart is emptied and filled again on the same page, they mount again and your scripts run again.
  • A script that has to run before the first add-to-cart (for example one that reacts to the theme or a quick-add button) belongs in Top of cart & general scripts. Script-only snippets go there too.
  • Scripts may not run in the admin preview. Test on your storefront.
  • Your markup is placed in a wrapper with the class ec-custom-html-container.

Guard against running twice

Because a script can run more than once per page, wrap it in a function and set a window flag around anything that must happen once, such as adding a listener. Keep the drawing part unguarded: after a remount your markup is new and empty again.

<div id="my-banner" class="tw-hidden tw-p-3 tw-text-center tw-bg-secondary tw-text-primary">
  Add one more item for free gift wrapping
</div>

<script>
  (function () {
    function update(cart) {
      var root = window.eliteCartDocumentOrShadowRoot || document;
      var banner = root.querySelector("#my-banner");
      if (!banner || !cart) return;
      banner.classList.toggle("tw-hidden", cart.item_count !== 1);
    }

    // Every run: draw the current cart into the (possibly new) markup.
    update(window.EliteCart.getCurrentCart());

    // Once per page: subscribe to cart changes.
    if (!window.__myBannerBound) {
      window.__myBannerBound = true;
      window.EliteCart.onCartChange(update);
    }
  })();
</script>

The first getCurrentCart() call matters: a conditional position mounts after the add-to-cart that filled the cart, so the change event for that add has already passed.

Widget mode

Widget mode is a checkbox on each position, next to Enable. Turn it on only for third-party widgets that scan the page for their markup, such as Trustpilot or Judge.me. With it on, your HTML is already on the page when it loads, so the widget's script finds it, and the cart then moves it into place. With it off (the default), the HTML is only added when the cart renders, which is faster.

Two things change in Widget mode:

  • Inline scripts run once when the page loads and again when the cart moves them into place. Guard them as shown above.
  • On the first run the cart may not have started yet. Wait for it before you call the API:
function whenEliteCartReady(callback) {
  if (window.EliteCart && window.EliteCart.getCurrentCart) return callback();
  setTimeout(function () { whenEliteCartReady(callback); }, 50);
}

Talk to the cart: the EliteCart API

The most useful calls inside Custom HTML (the full list is in the EliteCart API reference):

CallUse it to
window.EliteCart.getCurrentCart()Read the current cart (Shopify's cart format, prices in cents)
window.EliteCart.onCartChange(fn)Run fn(cart) on every cart change. Returns a function that removes the listener
window.EliteCart.refreshCart()Reload the cart from Shopify after you changed it
window.EliteCart.closeDrawer()Close the drawer, for example from your own close button
document event eliteCartRenderedRun code after the cart has rendered. event.detail.cart holds the cart

onCartChange can fire before the cart has redrawn. If you read or change rendered cart elements (a line item, the checkout button), listen to eliteCartRendered instead:

if (!window.__myLineTweaksBound) {
  window.__myLineTweaksBound = true;
  document.addEventListener("eliteCartRendered", function (event) {
    var root = window.eliteCartDocumentOrShadowRoot || document;
    root.querySelectorAll("li[data-line-key]").forEach(function (line) {
      // line.dataset.lineKey is the Shopify line item key
    });
  });
}

Change the cart from a script

Add ?elitecart=1 to every Shopify cart request your snippet makes, then refresh the cart yourself. Without it, Detect external cart changes treats your request like another app's and may refresh or reopen the drawer.

var res = await fetch(window.Shopify.routes.root + "cart/add.js?elitecart=1", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ items: [{ id: 12345678901234, quantity: 1 }] })
});
if (res.ok) await window.EliteCart.refreshCart();

Your own refreshCart() triggers onCartChange again, so keep an "is processing" flag if your change handler writes to the cart. Prices in the cart object are in cents. Format them with your store's currency rather than a hard-coded symbol (the AI generator adds a helper for this).

Checklist when a script doesn't work

  1. Open the browser console on your storefront and look for errors from your snippet.
  2. Replace every document.getElementById or document.querySelector that targets cart markup with window.eliteCartDocumentOrShadowRoot.
  3. Check the position: does it render on an empty cart? Does the script need the cart before the first add?
  4. Check that listeners are added once and the drawing code runs on every mount.
  5. For a third-party widget that stays blank: turn on Widget mode first.

Still stuck?

Chat with us and we'll help you get your cart live. Or email contact@elitecart.app.