Published 30 Dec 2020 · Updated 21 Oct 2026
A cart badge should reflect the server’s cart after an add request completes. Sending the write and read in parallel can render stale state; adding one to a returned count also breaks when a shopper selects a quantity greater than one.
Make the request order explicit
This example is for a Shopify theme with a standard product form. Add data-ajax-cart to that form, keep its existing variant and quantity fields, and include this live region inside it:
<p data-cart-status role="status" aria-live="polite"></p>
Mark each cart count with data-item-count. Load the following JavaScript once, after Shopify’s theme globals are available. Use it only on forms that do not already have a theme cart handler, to avoid duplicate add requests.
const root = window.Shopify.routes.root;
let cartBusy = false;
document.addEventListener('submit', async (event) => {
const form = event.target;
if (!form.matches('form[data-ajax-cart]')) return;
event.preventDefault();
if (cartBusy) return;
const status = form.querySelector('[data-cart-status]');
if (!status) return; // Include the live region shown above.
cartBusy = true;
const buttons = Array.from(form.querySelectorAll('[type="submit"]'));
const disabledBefore = buttons.map(button => button.disabled);
const data = new FormData(form);
buttons.forEach(button => { button.disabled = true; });
let added = false;
status.textContent = 'Adding item…';
try {
const response = await fetch(root + 'cart/add.js', {
method: 'POST',
headers: { Accept: 'application/json' },
body: data
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.description || 'Unable to add this item.');
}
added = true;
const cartResponse = await fetch(root + 'cart.js', {
headers: { Accept: 'application/json' },
cache: 'no-store'
});
if (!cartResponse.ok) throw new Error('Unable to refresh the cart.');
const cart = await cartResponse.json();
document.querySelectorAll('[data-item-count]').forEach(element => {
element.textContent = cart.item_count;
});
status.textContent = 'Item added to cart.';
} catch (error) {
status.textContent = added
? 'Item added, but the count could not refresh. Open your cart before trying again.'
: 'The request could not be completed. Check your cart before retrying. ' + error.message;
} finally {
cartBusy = false;
buttons.forEach((button, index) => { button.disabled = disabledBefore[index]; });
}
});
Why this sequence matters
Await the add request before reading the cart. Render the returned item_count directly. A shared in-flight guard prevents overlapping submissions through this handler, and submit buttons return to their original disabled state when the sequence ends.
A failed read after a successful write needs a different message: the item is already in the cart, so automatically repeating the write could add it twice. A network failure during the write also leaves the outcome uncertain; ask the shopper to check the cart before retrying.
Verification checklist
- Add quantities of one and several; compare the badge with the cart page.
- Submit twice quickly; verify this handler sends one add request.
- Simulate an unavailable variant and confirm the error is announced.
- Fail the cart read after a successful add; confirm no automatic second write occurs.
- Test a localized storefront and keyboard submission.
This example updates the count and status only. A theme cart drawer needs its own section refresh or documented integration. Coordinate other cart writers in the theme separately.
References: Shopify Ajax API and Cart API reference.