Back to Blog
Shopify Development

Shopify AJAX Cart Drawer Quantity Not Updating

Alex SterlingSeptember 27, 20267 min read
Screenshot of Shopify AJAX cart drawer showing quantity update issue

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

  1. Open the cart drawer (usually via data-cart-drawer-open).
  2. Click the “+” button on a line item.
  3. The button shows a loading spinner, then disappears.
  4. The quantity field still displays the old number, and the subtotal does not change.
  5. Console shows Uncaught TypeError: Cannot read property 'cart' of undefined or 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 a POST /cart/change.js request 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:

  1. cart.js is not loaded – the theme removed the {% render 'cart' %} snippet or the script tag was moved to the bottom of theme.liquid after a lazy‑load change.
  2. Incorrect selector in the event listener – the custom script still targets .ajax-cart-quantity while the theme renamed the class to .drawer-quantity during a recent UI overhaul.
  3. Missing data-cart-item-id attribute – the line‑item element no longer includes the required data-cart-item-id because a developer added a metafield loop that stripped out {{ item.key }}.
  4. Response handling overwritten – a third‑party app (e.g., a “Free Shipping Bar”) injects its own fetch('/cart/change.js') call and does not call window.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.cart object 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

  1. Open the drawer on a product page.
  2. Click “+” twice – the quantity should instantly become 3 (or whatever you clicked to).
  3. Observe the subtotal updating without a page reload.
  4. Open the browser console – there should be no errors related to cart/change.js.
  5. 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.js with a version number and update it only after testing.
  • Audit third‑party apps after each install. Look for any script that calls /cart/change.js and 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.

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.

Share
#Shopify AJAX cart drawer#Shopify cart quantity issue#Shopify liquid ajax update#Shopify theme debugging#Shopify cart drawer JavaScript#Shopify cart not updating#Shopify custom cart drawer
Y

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 →

Ready to start your project?

Get a free consultation and custom quote within 24 hours

No commitment required • Response within 2 hours