Cart Drawer Upsells Without Apps: Build High-Converting Bundles in Shopify Theme Code

Introduction
Adding targeted upsells in the cart drawer is one of the highest-ROI conversion moves for many Shopify merchants. This tutorial shows how to implement a shopify cart drawer upsell without apps by editing theme Liquid templates and adding a small JavaScript module. You’ll get code examples, UX patterns, tracking, and a simple experiment plan.
Why build upsells in the theme (not an app)
- Lower monthly costs and fewer external dependencies.
- Full control over UI and checkout flow consistency.
- Easier to integrate with theme variables, metafields, and custom logic.
However, remember security and upgrade complexity: theme code changes require care and versioning.
What we’ll build
- A cart drawer that shows dynamic product recommendations.
- Threshold-based messaging (e.g., "Add $X for free shipping" or "Add 1 more to get a bundle discount").
- Buttons to add recommended items into the drawer without a full page reload.
- Analytics events for tracking impressions, clicks, and conversions.
File changes overview
- Sections/cart-drawer.liquid (or snippet)
- Assets/cart-drawer.js
- Assets/cart-drawer.css (optional)
- theme.liquid or layout to include scripts
Implementation: Liquid + JavaScript
Below is a simplified pattern. Adjust to your theme structure (Dawn, Sense, etc.).
- cart-drawer.liquid (snippet or section)
<div id="cart-drawer" class="cart-drawer" aria-hidden="true"> <div class="cart-drawer__header"> <h2>Your cart</h2> <button class="cart-drawer__close" aria-label="Close">×</button> </div><div id="cart-items" class="cart-items">{% include 'cart-items' %}</div>
<div id="cart-upsell" class="cart-upsell" data-upsell-variants='{{ shop.metafields.custom.upsell_variants | json }}'> <!-- Fallback content: JavaScript will render recommendations here --> <div class="cart-upsell__loading">Loading recommendations…</div> </div>
<div class="cart-drawer__footer"> <div class="cart-total">Total: <span id="cart-total">{{ cart.total_price | money }}</span></div> <a href="/cart" class="btn btn--primary">View Cart</a> </div> </div>
Notes:
- We store recommended variant IDs in a metafield (shop.metafields.custom.upsell_variants) or use in-theme logic to render related product data server-side when possible.
- The JS will fetch product details via the Storefront or AJAX endpoints when needed.
- cart-drawer.js (client-side behaviors)
// assets/cart-drawer.js (function(){ const drawer = document.getElementById('cart-drawer'); const upsellContainer = document.getElementById('cart-upsell'); const closeBtn = drawer.querySelector('.cart-drawer__close');function openDrawer(){ drawer.setAttribute('aria-hidden', 'false'); } function closeDrawer(){ drawer.setAttribute('aria-hidden', 'true'); }
closeBtn.addEventListener('click', closeDrawer);
// Read upsell candidates from data attribute const upsellData = JSON.parse(upsellContainer.dataset.upsellVariants || '[]');
async function fetchProduct(variantId){ // Use AJAX cart or product endpoints to get minimal details // Example: fetch /products.json isn't available; instead fetch product by handle or use pre-rendered JSON in Liquid // For demo, assume theme stored a product-list JSON under window.UPSELL_PRODUCTS return window.UPSELL_PRODUCTS ? window.UPSELL_PRODUCTS[variantId] : null; }
function renderUpsells(items, cartTotal){ if(!items || items.length===0){ upsellContainer.innerHTML = ''; return; }
const listHtml = items.map(item=>{ const meetsThreshold = item.price + cartTotal >= item.threshold_amount; // example return ` <div class="upsell-card" data-variant="${item.id}"> <img src="${item.image}" alt="${item.title}" /> <div class="upsell-card__meta"> <div class="title">${item.title}</div> <div class="price">${item.price_display}</div> <button class="btn btn--small add-upsell">Add</button> ${ meetsThreshold ? '<div class="badge">Unlocks discount</div>' : '' } </div> </div>`; }).join(''); upsellContainer.innerHTML = `<div class="upsell-list">${listHtml}</div>`; attachAddHandlers(); // Track impression window.dataLayer && window.dataLayer.push({event: 'upsell_impression', upsell_count: items.length});}
function attachAddHandlers(){ upsellContainer.querySelectorAll('.add-upsell').forEach(btn=>{ btn.addEventListener('click', async function(e){ const variantId = this.closest('.upsell-card').dataset.variant; // Add to cart via AJAX await addItemToCart(variantId, 1); // Track click window.dataLayer && window.dataLayer.push({event: 'upsell_click', variant_id: variantId}); // Optionally refresh cart items refreshCartItems(); }); }); }
async function addItemToCart(variantId, qty){ return fetch('/cart/add.js', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({id: variantId, quantity: qty}) }).then(r=>r.json()); }
async function refreshCartItems(){ const res = await fetch('/cart?view=drawer'); // custom cart template that returns HTML const html = await res.text(); document.getElementById('cart-items').innerHTML = html; // Recalculate cart total for threshold logic const cartTotal = parseFloat(document.getElementById('cart-total').dataset.cents) || 0; // Render upsells again with updated cartTotal // In practice, fetch recommended products with prices and threshold logic server-side or via stored JSON const upsellProducts = (window.UPSELL_PRODUCTS_ARRAY || []).map(p=>({ id: p.id, title: p.title, image: p.image, price: p.price, price_display: p.price_display, threshold_amount: p.threshold_amount })); renderUpsells(upsellProducts, cartTotal); }
// Initial render document.addEventListener('DOMContentLoaded', function(){ // Example: window.UPSELL_PRODUCTS_ARRAY is prepopulated via Liquid const cartTotal = window.CART_TOTAL || 0; renderUpsells(window.UPSELL_PRODUCTS_ARRAY || [], cartTotal); }); })();
Product recommendation strategies
- Metafield lists: store a curated list of variant IDs in a shop or product metafield and surface them in the drawer.
- Related-by-collection: show top sellers from the same collection as the last added item.
- Frequently-bought-together: compute pairs server-side or via exports and store in theme data.
- Threshold-driven offers: e.g., "Add $15 for free shipping" or "Add one more for 20% off bundle".
Combining curated lists with dynamic thresholds often outperforms pure algorithmic recommendations for small catalogs.
Threshold logic patterns
- Free Shipping Threshold: calculate remaining amount to reach free shipping; show single low-cost item suggestions that close the gap.
- Bundle Discount Threshold: when cart contains X, recommend complementary Y with discount when both are in cart.
- Volume Upsell: "Add 2 for $X each" messaging for consumables.
Implementation tips:
- Keep calculations in cents to avoid floating errors.
- Prefer server-side calculation for checkout-affecting discounts (Shopify Scripts for Plus or Discount Codes for standard).
UX patterns and accessibility
- Make the add action reversible and obvious — show a mini toast "Added to cart" and allow removal.
- Keyboard focus management: when the cart drawer opens, trap focus inside and return focus when closed.
- Screen reader announcements: announce upsell addition and updated totals.
- Avoid surprising upsells: clearly show price and inventory.
Include aria attributes and follow W3C Web Accessibility Initiative guidelines for drawer components.
Comparison: App-based vs Theme-based cart drawer upsell
Here’s a short comparison to help you decide which approach to use.
| Feature | App-based upsell | Theme-based (no app) |
|---|---|---|
| Monthly cost | App fee (ongoing) | No extra cost (one-time dev) |
| Performance impact | Potential third-party script | Controlled by theme (smaller) |
| Custom UI flexibility | Limited by app | Full control via Liquid + CSS |
| Maintenance | App updates managed externally | Requires developer to update |
Tracking & analytics
Key events to track (push to dataLayer or your analytics):
- upsell_impression — when recommended items rendered
- upsell_click — user clicks "Add" on a recommended product
- upsell_add_confirm — item successfully added
- upsell_conversion — recommended SKU purchased in the same session
Example dataLayer push on add (already shown in JS). Connect these to Google Analytics / GA4 or measurement platform. Follow Google Search Central and Google Lighthouse best practices to minimize performance penalties.
A/B test and validation plan
- Hypothesis: Adding curated suggestions in the cart drawer increases AOV by X% without hurting conversion rate.
- Metrics: AOV, conversion rate, add-to-cart rate for recommendations, bounce rate.
- Sample: Run the experiment for a full business cycle (at least 2 weeks) and use statistical significance tools.
Use server-side flags or client-side experiment tools to toggle the drawer upsell for treatment vs control.
Real-World Scenarios
Scenario 1: The Niche Cosmetics Store
A small cosmetics brand added a single curated face-serum to the cart drawer as a recommended add-on. By keeping imagery and copy concise, they increased average order value and avoided app fees. They pre-rendered product data via metafields and saw minimal performance impact.
Scenario 2: The Outdoor Gear Retailer
An outdoor retailer implemented a "add $25 for free shipping" threshold in the cart drawer. They suggested small but relevant accessories (carabiner, socks). The threshold logic was calculated in theme code and tracked via dataLayer to measure conversion lift.
Scenario 3: The Subscription Starter Kit
A merchant selling consumables suggested a 2-pack discount in the drawer (volume upsell). They used Liquid to show discounted price messaging and JS to update the cart. Results showed improved repeat purchase likelihood for customers who accepted the bundle.
Checklist
Checklist
- Back up current theme and work in a development theme
- Decide how recommendations are sourced (metafields, collection, analytics)
- Pre-render product metadata in Liquid when possible
- Implement accessible drawer (focus trap, aria attributes)
- Add analytics events: impression, click, add, conversion
- Test add-to-cart flows, edge cases (sold out, inventory changes)
- Run A/B test and collect at least two weeks of data
Latest News & Trends
- Headless and hybrid architectures are making theme-level experiments lighter and more performant.
- Privacy-first tracking changes (cookieless) increase the importance of server-side attribution and first-party events.
- Performance budgets now factor into UX decisions: prefer server-rendered micro-data and minimal client JS.
Comparison table: Tracking approaches
Below is a short table comparing tracking approaches for upsell events.
| Approach | Complexity | Reliability |
|---|---|---|
| Client-side dataLayer push | Low | Medium (adblockers) |
| Server-side events (webhooks) | High | High |
| Hybrid (client + server) | Medium | High |
Key implementation tips
- Precompute recommended product JSON in Liquid and expose as a JS variable to avoid product endpoint latency.
- Keep UI copy concise; show clear price and benefits.
- Use thresholds that match shipping/discount rules to avoid mismatched expectations.
Conclusion
Implementing a shopify cart drawer upsell without apps is entirely feasible and often preferable for merchants who want fast, tailored UI and lower recurring costs. By combining Liquid pre-rendered data, a small, focused JavaScript module, clear threshold rules, and analytics eventing, teams can deliver high-converting bundles with controlled performance.
External resources
- W3C Web Accessibility Initiative — accessibility guidance for interactive UI
- Google Lighthouse — measure performance impact of added scripts
- Mozilla MDN Web Docs — JavaScript and web API references
About Prateeksha Web Design
Prateeksha Web Design builds conversion-focused Shopify themes and experiments, specializing in theme-level upsells, custom Liquid code, and performance-conscious UX for ecommerce brands.
Chat with us now Contact us today.