Skip to content

How to Build a Shopify AJAX Cart Drawer in Liquid

Build a custom Shopify AJAX cart drawer in a Liquid theme using native cart endpoints, section rendering, and accessible UX patterns without adding another app.

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:

  1. Better performance control.
  2. Cleaner design consistency with your theme.
  3. Less script bloat.
  4. 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:

  1. A drawer section (cart-drawer.liquid).
  2. JavaScript handlers for add/change/remove actions.
  3. 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:

  1. Move focus into drawer on open.
  2. Return focus to trigger button on close.
  3. Close on Esc.
  4. Prevent background scroll while open.
  5. Announce cart updates via an aria-live region.

These details improve usability and conversion.

Common mistakes to avoid

  1. Updating only local HTML and forgetting server state.
  2. Not handling sold-out/invalid variant responses.
  3. Missing race-condition handling on rapid clicks.
  4. 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