Custom Shopify Cart drawer

Hi,
I want to know what some best practices are for building a custom Shopify cart drawer that stays fast and doesn’t conflict with other apps.
Your response will really help me learn more about this.

Hi @olivianewuser123 Welcome To Shopify Community So A few practices that consistently help with both speed and app compatibility:

Use cart.js (the Ajax Cart API) instead of full page reloads for every cart action, add-to-cart, quantity change, remove should all hit the Ajax endpoints and re-render just the drawer, not the whole page. This is the single biggest speed factor.

Listen for cart updates rather than owning the cart state exclusively. A lot of conflicts happen because custom drawers assume they’re the only thing modifying the cart, so when an app (like a discount or upsell app) updates the cart via its own Ajax call, your drawer doesn’t know to re-render and shows stale data. Dispatching and listening for a custom event (many themes use something like cart:updated or similar) whenever the cart changes, and having your drawer re-fetch/re-render on that event rather than only on its own actions, avoids this.

Avoid deeply nested custom cart logic that duplicates what Shopify’s cart object already provides, pull directly from the cart object’s items, item_count, and total_price rather than maintaining a separate JS state that can drift out of sync with the actual cart.

For performance specifically, lazy-load the drawer’s HTML/JS only when it’s first opened rather than on every page load, and keep product images in the drawer small/optimized since cart drawers often load many thumbnails at once.

Test explicitly with whatever upsell/discount/subscription apps you’re running (or plan to run), since app-added line items, bundled products, or free gifts are the most common source of drawer bugs, apps that inject cart items sometimes don’t trigger the same update events a manual add-to-cart does.

Hope this helps, and if it does, don’t forget to like and mark it as the solution. Thank you!

Adding to @Mustafa_Ali’s answer rather than replacing it, because Shopify
standardised this in June and it turns his advice into something you can
follow exactly.

The thing that catches custom drawers specifically: apps can now call
Shopify.actions.updateCart on any Liquid storefront with no setup from you.
It only refreshes the cart in place if it recognises your markup, either
Horizon-style (a with renderCartBubble()) or Dawn-style (a
cart-drawer or cart-items whose prototype has getSectionsToRender()). It
checks for those methods, not the theme name. A drawer you wrote yourself
usually matches neither, and the documented fallback is a full page reload.

So here is the order I would do it in.

  1. Stop apps forcing a reload. In theme.liquid, above {{ content_for_header }}:
document.addEventListener('DOMContentLoaded', () => {
  Shopify.actions.updateCart.configure({
    eventTarget: () => document.querySelector('your-drawer-element'),
  });
});

Any configuration at all turns the reload fallback off. Only the first
configure call on an action takes effect, which is why it goes above
content_for_header.

  1. Write one refresh function and let nothing else repaint. It fetches
    /cart.js and re-renders the drawer. Your own add, remove and quantity
    handlers call it. That is Mustafa’s “don’t own the cart state” point made
    concrete.

  2. Point the standard cart events at that same function, so changes an app
    made go through the identical path as your own:

['lines-update', 'discount-update', 'attributes-update', 'note-update']
  .forEach(e => document.addEventListener('shopify:cart:' + e, refreshCart));
  1. Halve your own requests. /cart/add.js, /cart/change.js, /cart/update.js
    and /cart/clear.js all accept a sections parameter, so you get the
    re-rendered section HTML back in the same response as the cart change.
    One round trip instead of two.

  2. Turn on one upsell or discount app and add something from it,
    not from your drawer. You want no full page reload and the new line
    appearing in the drawer. If the page reloads, step 1 is pointing at the
    wrong element.

If you need more help, let me know, or if this was useful feel free to mark as a solution

Hi @karim326 Good addition, this is more current than what I had — didn’t realize Shopify standardized Shopify.actions.updateCart back in June. The event-target routing and section-response round-trip halving are solid points, appreciate the detail.

The biggest thing I’d watch is how the drawer gets its data, because a lot of custom drawers get slow when they re-render too much of the page or make extra requests every time someone changes quantity.

On stores I’ve worked on, the cleanest setup was to keep the drawer update isolated to the cart state only, then reuse Shopify’s cart endpoints for add, update, and remove so you’re not fighting whatever else is on the page.

I’d keep the drawer markup small, so avoid loading app scripts inside it unless they truly need to live there. And test it with your heaviest app stack turned on, because that’s usually where conflicts could show up.

Also, to make it as stable as possible, give every drawer element a unique hook and don’t rely on generic class names that another app might also target.

Hey @olivianewuser123, the app conflicts almost always come from rendering cart lines in JavaScript. Other apps add free gifts, bundle children and discount lines server-side, so your JS-built markup either misses them or shows stale prices. Pass a sections parameter to /cart/add.js, /cart/change.js and /cart/update.js, then swap in the HTML Shopify returns for your drawer section instead of building the line items yourself. It’s the same request, so there’s no second fetch of /cart.js and it stays fast. To confirm it works, install a free-gift app, add its trigger product, and watch the gift line show up in the drawer without a refresh.

@Mustafa_Ali @karim326
Thanks for your contribution! I am a little confused about how to mark both of your answers as solutions, but I really appreciate both of your responses. Thanks again!

I’ll make sure to use unique data-attributes for selectors to prevent any app script collisions.
Thanks @DanielAnderson for taking the time to share this.

Sure. I’d keep it simple and genuinely useful:

A few things I’d keep in mind when building a custom cart drawer:

Keep the JavaScript lightweight and avoid unnecessary polling or repeated cart requests.
Use Shopify’s cart endpoints/events carefully so the drawer stays in sync with cart changes.
Avoid overriding global theme/app functions where possible, since that can create conflicts with other apps.
Load only the scripts and assets the cart drawer actually needs.
Test with your main apps enabled, especially upsell, discount, subscription, and shipping apps, since these can also modify cart behavior.

I’d also recommend testing each integration separately when adding apps, because it makes conflicts much easier to identify.