Skip to content

Open your theme's cart drawer after a quiz add

Goal: the shopper clicks “Add to cart” on a recommended product, the line lands in the Shopify cart, and the theme’s cart drawer opens with it, the way it does for the theme’s own add-to-cart button.

Uses: Storefront JS API, cart.added, Shopify’s /cart.js route, and the theme’s own cart event or drawer element. Nothing to install; no API key. Events fire on the Plus and Enterprise plans.

The quiz adds through Shopify’s cart API (/cart/add.js), so the cart is right: the line is there on the next page load and in the checkout. What the theme does about it on the current page is up to the theme. After each add the quiz updates the common cart-count badges, repaints a Dawn drawer’s contents in place without opening it, and dispatches the generic cart:refresh and cart:updated events on document. Themes that listen to something else keep their drawer and count as they were until the page reloads. Horizon-family themes (Shopify’s Horizon, Foxecom’s Pebble, and other themes built on the same architecture) are the usual case: their <cart-drawer-component> and <cart-count> react only to the CartUpdateEvent from their own @theme/events module, which the page resolves through an import map.

cart.added fires once the lines are in the cart, with items[] of { variant_id, product_id, quantity, selling_plan_id, product }. It fires for the single-card add and for the add-all button, on the web and inside the Tapcart app. It does not fire when the add failed; cart.failed does, with a code.

Read the cart back from Shopify and dispatch the theme’s own event with it. The import() resolves @theme/events through the page’s import map, the same way the theme’s scripts do.

<script>
window.octaneai = window.octaneai || [];
window.octaneai.push(function (octaneai) {
var root = (window.Shopify && window.Shopify.routes && window.Shopify.routes.root) || '/';
octaneai.on('cart.added', function () {
fetch(root + 'cart.js', { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (cart) {
return import('@theme/events').then(function (events) {
document.dispatchEvent(new events.CartUpdateEvent(cart, 'quiz', { itemCount: cart.item_count }));
});
})
.catch(function (e) { console.warn('cart drawer not refreshed:', e); });
});
});
</script>

The drawer opens with the line and the header count updates. The source argument ('quiz' here) is any string; the theme uses it to tell its own updates apart from others.

Dawn’s <cart-drawer> (and <cart-notification> on stores that use the popup instead) re-renders itself from Shopify’s section rendering and opens in one call, renderContents(). The quiz already repaints the drawer’s contents after an add; this opens it.

<script>
window.octaneai = window.octaneai || [];
window.octaneai.push(function (octaneai) {
var root = (window.Shopify && window.Shopify.routes && window.Shopify.routes.root) || '/';
octaneai.on('cart.added', function (event) {
var drawer = document.querySelector('cart-drawer, cart-notification');
if (!drawer || typeof drawer.renderContents !== 'function') return;
var ids = drawer.getSectionsToRender().map(function (s) { return s.id; });
fetch(root + '?sections=' + ids.join(','), { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (sections) {
var variant = Number(String(event.data.object.items[0].variant_id).replace(/^.*\//, ''));
drawer.renderContents({ id: variant, sections: sections });
})
.catch(function (e) { console.warn('cart drawer not refreshed:', e); });
});
});
</script>

renderContents wants the numeric variant id of the added line (cart-notification shows that product); cart.added carries it as a GID, so the snippet strips the prefix.

Many themes re-render their mini-cart on a cart:refresh event on document. The quiz already dispatches it after every add, so if the drawer still does not react, the theme is listening to something else: look in its cart JavaScript for the addEventListener call that refreshes the drawer, and dispatch that event from cart.added instead. A theme that only opens its drawer from its own button usually exposes an open() method on the drawer element, which the same handler can call after the refresh.

<script>
window.octaneai = window.octaneai || [];
window.octaneai.push(function (octaneai) {
octaneai.on('cart.added', function () {
document.dispatchEvent(new CustomEvent('cart:refresh', { bubbles: true }));
});
});
</script>

Open the store, view the page source (or the Elements panel), and look for these:

You findThe theme isUse
<cart-drawer-component> and a <script type="importmap"> that maps @theme/eventsHorizon family (Horizon, Pebble, …)the Horizon snippet
<cart-drawer> or <cart-notification>, and assets/cart-drawer.jsDawn family (Dawn, Sense, Craft, Refresh, …)the Dawn snippet
NeitherSomething elsethe cart:refresh snippet, then the theme’s own event if that does nothing

One snippet per store: paste it in theme.liquid right after the quiz embed’s <script> tag, or in the page’s Custom Liquid section below the quiz. It runs from window.octaneai.push, so it registers whether the quiz has loaded yet or not.

  • import('@theme/events') on a theme without the import map entry rejects with a resolution error; the catch logs it and the cart is still right. Feature-detect with document.querySelector('script[type="importmap"]') if one snippet has to serve several themes.
  • /cart.js and /?sections= are storefront routes: on a page off the store’s domain they return the wrong thing or 404, and the quiz’s own add does not work there either.
  • A cart.failed add never reaches the handler; the quiz shows its own error to the shopper and the drawer stays as it was.
  • cart.added fires after the cart call returned, so the /cart.js read in the handler already includes the line. Reading the cart back, rather than passing the event’s items, keeps the theme’s totals and shipping bar right when the shopper already had lines in the cart.
  • Inside the Tapcart app the add goes through Tapcart’s bridge and the native cart shows it; there is no theme drawer to open, and the snippet does nothing useful there.
  • The add-all-to-cart button and a multi-item checkout report all their lines in one cart.added; the handler runs once.