A cart drawer is one of the highest-impact UX features in ecommerce. It lets customers add products, confirm details, and keep shopping without a full page reload.
You can build it directly in your Shopify Liquid theme with native endpoints. No cart app required.
Why build it yourself
App-based cart drawers can be great, but custom implementation gives you:
- Better performance control.
- Cleaner design consistency with your theme.
- Less script bloat.
- Fewer dependency conflicts.
If your store already has a custom theme, this approach is usually worth it.
Architecture in plain English
You need three parts:
- A drawer section (cart-drawer.liquid).
- JavaScript handlers for add/change/remove actions.
- Section re-rendering so the drawer and cart badge stay current.
Step 1: Add-to-cart via Shopify AJAX endpoint
async function addToCart(variantId, quantity = 1) {
const response = await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity }]
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.description || 'Unable to add item to cart.');
}
await refreshCartUI();
openCartDrawer();
}Code language: JavaScript (javascript)
Hook this to your product form submit event and prevent default page reload.
Step 2: Re-render drawer and icon bubble
Use section rendering to keep server-rendered Liquid in sync.
async function refreshCartUI() {
const sections = 'cart-drawer,cart-icon-bubble';
const response = await fetch(`${window.location.pathname}?sections=${sections}`);
const htmlBySection = await response.json();
Object.entries(htmlBySection).forEach(([sectionId, html]) => {
const container = document.querySelector(`[data-section-id="${sectionId}"]`);
if (container) container.innerHTML = html;
});
}Code language: JavaScript (javascript)
In your markup, add wrappers:
<div data-section-id="cart-drawer">
{% section 'cart-drawer' %}
</div>
<div data-section-id="cart-icon-bubble">
{% section 'cart-icon-bubble' %}
</div>Code language: Django (django)
Step 3: Quantity updates and remove actions
Use change.js for both quantity changes and removals.
async function updateLineItem(line, quantity) {
const response = await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ line, quantity })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.description || 'Unable to update cart.');
}
await refreshCartUI();
}Code language: JavaScript (javascript)
quantity: 0 removes the line item.
Step 4: Accessibility and UX details that matter
A custom drawer should also:
- Move focus into drawer on open.
- Return focus to trigger button on close.
- Close on Esc.
- Prevent background scroll while open.
- Announce cart updates via an aria-live region.
These details improve usability and conversion.
Common mistakes to avoid
- Updating only local HTML and forgetting server state.
- Not handling sold-out/invalid variant responses.
- Missing race-condition handling on rapid clicks.
- Ignoring keyboard interaction patterns.
Conclusion
A native AJAX cart drawer gives you app-level UX without app-level overhead. You keep full control over performance, styling, and behavior while relying on Shopify’s built-in cart APIs.
If your theme is already custom, this is usually the right long-term move.
Leave a Comment