Shopify AJAX Cart Drawer Quantity Not Updating

Shopify cart quantity not updating in the AJAX cart drawer
Customers can add a product, open the drawer, click the “+” or “–” button, but the number never changes and the total stays the same. The drawer closes, the main cart page shows the correct amount, yet the drawer stays stale. This post walks you through the exact cause and gives you a copy‑and‑paste fix so the drawer reflects the new quantity immediately.
Symptom
- Open the cart drawer (usually via
data-cart-drawer-open). - Click the “+” button on a line item.
- The button shows a loading spinner, then disappears.
- The quantity field still displays the old number, and the subtotal does not change.
- Console shows
Uncaught TypeError: Cannot read property 'cart' of undefinedor a 404 on/cart/update.js.
If you see any of the above, you are dealing with the classic AJAX drawer desync bug.
Cause
Shopify’s default AJAX cart drawer relies on two moving parts:
cart.js– the library that sends aPOST /cart/change.jsrequest and updates the DOM.- Theme’s custom JavaScript – often a thin wrapper that re‑renders the drawer after the response.
The most common reasons the quantity never updates are:
cart.jsis not loaded – the theme removed the{% render 'cart' %}snippet or the script tag was moved to the bottom oftheme.liquidafter a lazy‑load change.- Incorrect selector in the event listener – the custom script still targets
.ajax-cart-quantitywhile the theme renamed the class to.drawer-quantityduring a recent UI overhaul. - Missing
data-cart-item-idattribute – the line‑item element no longer includes the requireddata-cart-item-idbecause a developer added a metafield loop that stripped out{{ item.key }}. - Response handling overwritten – a third‑party app (e.g., a “Free Shipping Bar”) injects its own
fetch('/cart/change.js')call and does not callwindow.ShopifyCart.updateCart.
Common mistake
Developers often copy the vanilla Shopify snippet and then wrap the whole drawer in a <div id="AjaxCartDrawer">. When the drawer is re‑rendered via innerHTML, all previously attached event listeners are lost, so the next click does nothing. The fix is to re‑bind listeners after each render or use event delegation.
Fix
Below is a step‑by‑step, copy‑ready solution that works on any OS 2.0‑compatible theme.
1. Verify cart.js is present
Open Online Store → Themes → Actions → Edit code and locate layout/theme.liquid. Ensure the following line appears before the closing </body> tag:
{{ 'cart.js' | asset_url | script_tag }}
If it is missing, add it exactly as shown and save.
2. Add a robust drawer script
Create a new asset called ajax-drawer.js (or edit an existing custom JS file) and paste the code below. It:
- Delegates clicks from the document level (so re‑rendering does not break listeners).
- Falls back to the native
Shopify.cartobject if a third‑party app interferes. - Updates the drawer DOM with the fresh cart JSON.
/* ajax-drawer.js – robust quantity updater */
document.addEventListener('click', function (e) {
const plusBtn = e.target.closest('.drawer-quantity-plus');
const minusBtn = e.target.closest('.drawer-quantity-minus');
if (!plusBtn && !minusBtn) return;
const lineItem = e.target.closest('[data-cart-item-id]');
if (!lineItem) return;
const key = lineItem.dataset.cartItemId;
const input = lineItem.querySelector('input.drawer-quantity-input');
let quantity = parseInt(input.value, 10);
quantity = plusBtn ? quantity + 1 : Math.max(quantity - 1, 0);
// Disable button while request is in flight
plusBtn?.setAttribute('disabled', true);
minusBtn?.setAttribute('disabled', true);
fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: key, quantity: quantity })
})
.then(response => response.json())
.then(data => {
// Update drawer markup – replace only the items container
const container = document.querySelector('#AjaxCartDrawer .drawer-items');
if (container) {
container.innerHTML = data.items.map(item => `
<div class="drawer-item" data-cart-item-id="${item.key}">
<img src="${item.image}" alt="${item.title}" class="drawer-item-img"/>
<div class="drawer-item-info">
<p class="drawer-item-title">${item.title}</p>
<div class="drawer-quantity">
<button class="drawer-quantity-minus">−</button>
<input type="number" class="drawer-quantity-input" value="${item.quantity}" min="0"/>
<button class="drawer-quantity-plus">+</button>
</div>
</div>
</div>`).join('');
}
// Update totals
document.querySelector('#AjaxCartDrawer .drawer-subtotal').textContent = Shopify.formatMoney(data.total_price, Shopify.money_format);
})
.catch(err => console.error('Cart update failed', err))
.finally(() => {
plusBtn?.removeAttribute('disabled');
minusBtn?.removeAttribute('disabled');
});
});
3. Include the script in your theme
Add the following tag right after the cart.js line you verified in step 1:
{{ 'ajax-drawer.js' | asset_url | script_tag }}
4. Ensure the markup matches the selectors
Open the drawer snippet (commonly snippets/cart-drawer.liquid) and make sure each line‑item has the required attributes and class names:
{% for item in cart.items %}
<div class="drawer-item" data-cart-item-id="{{ item.key }}">
<img src="{{ item.image | img_url: '100x' }}" alt="{{ item.title }}" class="drawer-item-img"/>
<div class="drawer-item-info">
<p class="drawer-item-title">{{ item.title }}</p>
<div class="drawer-quantity">
<button class="drawer-quantity-minus" aria-label="Decrease quantity">−</button>
<input type="number" class="drawer-quantity-input" value="{{ item.quantity }}" min="0"/>
<button class="drawer-quantity-plus" aria-label="Increase quantity">+</button>
</div>
</div>
</div>
{% endfor %}
If your theme uses different class names, rename the selectors in ajax-drawer.js accordingly.
5. Clear any conflicting app scripts
If you have an app that also injects fetch('/cart/change.js') (e.g., a “Buy‑Now” button app), open Apps → Manage private apps → Edit and look for a script tag that contains cart/change.js. Either disable the app’s cart‑override feature or add the following line before the app’s script tag:
<script>
window.ShopifyCart = window.ShopifyCart || {};
window.ShopifyCart.updateCart = function(){ /* no‑op – our drawer handles updates */ };
</script>
This prevents the app from overwriting the native Shopify.cart handler.
Verify
- Open the drawer on a product page.
- Click “+” twice – the quantity should instantly become 3 (or whatever you clicked to).
- Observe the subtotal updating without a page reload.
- Open the browser console – there should be no errors related to
cart/change.js. - Refresh the page, reopen the drawer – the quantity should persist (Shopify’s cart state is server‑side, so a refresh confirms the backend update).
Prevent future desyncs
- Never hard‑code selectors – always use data attributes (
data-cart-item-id) and keep class names in a single source of truth (e.g., a SCSS variable). - Lock the drawer script version – add a comment at the top of
ajax-drawer.jswith a version number and update it only after testing. - Audit third‑party apps after each install. Look for any script that calls
/cart/change.jsand ensure it respects your custom handler. - Use event delegation (as in the provided script) instead of binding directly to buttons. This survives any DOM replacement the theme may perform.
- Run a nightly Lighthouse CI that includes a step to click the drawer buttons. If the test fails, you’ll catch a regression before it reaches customers.
Frequently asked questions
Does this work with Shopify 2.0 sections?
Yes. The script targets elements by data attributes, which are preserved across section renders in OS 2.0.
My theme uses a modal instead of a drawer – can I still use this code?
Absolutely. Replace #AjaxCartDrawer with the ID of your modal container and adjust the selector names accordingly.
Will this break the native Shopify “Buy it now” button?
No. The fix only intercepts fetch('/cart/change.js') when the request originates from the drawer. The “Buy it now” flow uses a different endpoint (/checkout), so it remains untouched.
I’m using a custom checkout app – do I need to modify anything?
Only if the app injects its own cart‑change script. Follow step 5 to neutralise the conflict.
Related reading
- Discount codes not applying at checkout
- Fixing layout shift on collection pages
- Shopify conversion optimization — our service page
Need a hand?
If you prefer a developer to implement or audit the drawer for you, Yaxify can get it done in a single sprint. Contact us and we’ll have your cart updating flawlessly across all devices.
The Yaxify Team
Shopify & Web Development Agency
We build custom Shopify stores and high-performance websites for growing businesses across the USA, UK, and UAE. Everything we publish comes out of client work we have actually shipped.
More about Yaxify →