How are you handling add-to-cart these days — full page reload or AJAX drawer?

Cart UX chat for the theme devs :shopping_cart:

I’ve mostly moved to AJAX add-to-cart with a slide-out drawer so people never leave the page they’re on. Feels smoother, but it does mean more JS to babysit (updating the cart count, handling errors, etc.).

How are you doing it? Still using the theme’s native cart, or rolling your own AJAX with the /cart/add.js endpoint? Any gotchas you ran into with the drawer approach?

Hi @Rasel

I have mostly used Shopifys native cart features with AJAX for the add-to-cart process of creating the whole cart system again from the beginning. The slide-out drawer definitely makes the user experience better.

The main things I have noticed that are important to watch are updates to the cart count errors with variants or add-to-cart actions changes to quantities and making sure the drawer state stays the same after AJAX requests. App integrations can also cause problems sometimes so I like to keep the custom JavaScript simple, as possible and use Shopifys native cart endpoints and events whenever I can.

The biggest gotcha for me was re-rendering the drawer after the add. Instead of hand-syncing the count and line items, pass sections=cart-drawer,cart-icon-bubble in the /cart/add.js request and Shopify returns the rendered HTML for those sections, which you swap straight into the DOM. Also remember subscription variants need selling_plan in the payload or the add fails outright, and some apps hook the form submit, so bind to the fetch completing rather than the button click.

@Rasel ,
For most projects I stick with the themes native AJAX cart unless the client has very specific requirements. It tends to be more stable and is less likely to break with theme updates.

The biggest challenge with custom cart drawers isnt adding products is keeping everything in sync. Cart count line item updates, discounts selling plans, gift cards, and app integrations all need to refresh correctly. I have also run into issues with race conditions when customers click Add to Cart multiple times quickly.

If you do build your own I’d recommend relying on Shopifys Cart API as much as possible and keeping the custom JavaScript lightweight. It makes long-term maintenance much easier.

the “bind to the fetch completing rather than the button click” bit in #4 is the right instinct but fetch isnt the only transport you have to cover. plenty of themes and a fair few apps still add through XMLHttpRequest, so if you only patch fetch those adds are invisible to you and the drawer just never opens. you end up patching XHR as well, both open, to stash the url, and send, to hang a loadend listener on it.

then theres the third path, the plain form post with no ajax at all. listening isnt enough there, because ajax themes install their own submit handler and yours runs after theirs — a motion style theme will have already sent the shopper to /cart by the time you react. taking it over means a capture phase listener on document with preventDefault and stopPropagation, and then you do the post yourself.

two traps show up the moment you have all three running. one, a single add can trip more than one detector, so you resync twice and the drawer opens twice — worth putting a single claim token per mutation cycle in front of it, first detector wins and the others no-op. two, your own post goes out through the fetch you just patched, so you re-enter your own interceptor. grab a reference to the native fetch before you patch it and use that one internally.

on selling_plan its a bit worse than “include it or the add fails”. some themes render several selling_plan inputs, one per plan, with the inactive ones empty. read the form naively and you pick up an empty one, the add succeeds, and the subscription quietly becomes a one-time purchase, with no error anywhere to tell you. take the last non-empty value.

last thing, less js and more scope: buy it now and shop pay on the product page skip the cart entirely, so nothing you build in the drawer runs for those shoppers at all. thats fine, just size it before you promise anyone cart level offers.

disclosure, i build verve, a cart drawer app, so most of the above is scar tissue from other peoples themes rather than theory.

Hi there @Rasel
AJAX cart panels are my default for most stores that I install it on because it’s less distracting to the customer and generally results in a smoother buying experience. I stick with Shopify’s Cart API and just ensure that I’ve acounted for error handling, inventory updates, selling plans, and accessibility.
The biggest gotchas are:

  1. syncing cart state across tabs,
  2. drawer compatibility with dynamic checkout and bundled products.

Thanks for sharing your experience! I completely agree—using Shopify’s native AJAX cart endpoints is usually the most reliable approach, especially for long-term compatibility with themes and apps.

You’ve also highlighted some of the trickiest parts: keeping the cart count in sync, handling variant changes correctly, and preserving the drawer state after cart updates. Those small details can have a big impact on the overall user experience.

I’ve also run into cases where third-party apps interfere with custom cart logic, so keeping the JavaScript as simple as possible and relying on Shopify’s built-in cart APIs has worked best for me too. Thanks for the valuable insights!

Thanks for sharing your thoughts, Steve!

I agree that a well-implemented AJAX cart drawer provides a much smoother shopping experience than redirecting customers to the cart page. You also brought up some important points about the maintenance side of things—keeping the cart state synchronized, handling inventory errors, selling plans, and ensuring compatibility with third-party apps can definitely become challenging.

I also like your approach of building on top of an existing theme’s cart drawer instead of replacing it whenever possible. It usually leads to better compatibility and makes future theme updates much easier to manage.

Thanks again for the detailed insights. It’s great to hear how other developers are balancing UX improvements with long-term maintainability.

Thanks for the great tips! Passing sections=cart-drawer,cart-icon-bubble with the /cart/add.js request is a really useful approach. Letting Shopify return the updated section HTML is much cleaner than manually syncing the cart count and drawer contents, and it helps avoid UI inconsistencies.

Good point about subscription products as well—it’s easy to overlook the selling_plan parameter, and that can definitely cause add-to-cart requests to fail. I also agree that listening for the completed AJAX request instead of the button click makes integrations with third-party apps much more reliable.

Really appreciate you sharing these practical insights!

Thanks for sharing your experience! I completely agree that building on top of the theme’s native AJAX cart is often the safest and most maintainable approach, especially when there aren’t any unique client requirements.

You also made a great point that the real challenge isn’t the add-to-cart request itself—it’s keeping everything synchronized afterward. Discounts, selling plans, gift cards, app integrations, and even rapid multiple clicks can introduce edge cases that are easy to miss.

Relying on Shopify’s Cart API and keeping the custom JavaScript as lightweight as possible has been my experience as well. Thanks for adding these valuable insights to the discussion! :blush:

Thanks for the detailed insights! You brought up several edge cases that are easy to overlook, especially around XHR, form submissions, duplicate events, and selling_plan handling. The reminder about Buy It Now and Shop Pay bypassing the cart is also a great point. Really appreciate you sharing your real-world experience!

Thanks for sharing! I agree—AJAX cart drawers usually provide a much smoother shopping experience. You also highlighted some important edge cases, especially keeping the cart state in sync across tabs and ensuring compatibility with dynamic checkout and bundled products. Appreciate the insights!