Cross-Sells Inside Cart Drawer
Implement cross-sell and upsell recommendations in the cart drawer to increase average order value with relevant product suggestions.
Cross-sells in the cart are a powerful way to increase average order value. When customers are ready to buy, showing relevant complementary products can encourage additional purchases.
Cross-Sell Strategies
- Complementary products: Items that go with cart contents
- Frequently bought together: Based on purchase history
- Threshold incentives: “Add $X more for free shipping”
- Bundle suggestions: Complete the set
- Accessories: Related add-ons
Basic Cart Cross-Sell
{# snippets/cart-cross-sells.liquid #}
{%- if cart.item_count > 0 -%} {%- assign cross_sell_products = collections['accessories'].products | limit: 4 -%}
{%- if cross_sell_products.size > 0 -%} <div class="cart-cross-sells"> <h3 class="cart-cross-sells__heading">Complete Your Order</h3>
<div class="cart-cross-sells__products"> {%- for product in cross_sell_products -%} {%- unless cart.items | map: 'product_id' | contains: product.id -%} {% render 'cart-cross-sell-item', product: product %} {%- endunless -%} {%- endfor -%} </div> </div> {%- endif -%}{%- endif -%}Cross-Sell Item Component
{# snippets/cart-cross-sell-item.liquid #}
{%- assign variant = product.selected_or_first_available_variant -%}
<div class="cross-sell-item"> <a href="{{ product.url }}" class="cross-sell-item__image"> <img src="{{ product.featured_image | image_url: width: 150 }}" alt="{{ product.featured_image.alt | default: product.title }}" width="75" height="75" loading="lazy" > </a>
<div class="cross-sell-item__info"> <a href="{{ product.url }}" class="cross-sell-item__title"> {{ product.title }} </a> <p class="cross-sell-item__price"> {{ variant.price | money }} </p> </div>
{%- if variant.available -%} <button type="button" class="cross-sell-item__add button button--small" data-add-to-cart data-variant-id="{{ variant.id }}" > Add </button> {%- else -%} <span class="cross-sell-item__sold-out">Sold out</span> {%- endif -%}</div>Quick Add JavaScript
document.querySelectorAll('.cross-sell-item__add').forEach((button) => { button.addEventListener('click', async () => { const variantId = button.dataset.variantId;
button.disabled = true; button.textContent = 'Adding...';
try { await fetch('/cart/add.js', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: [{ id: variantId, quantity: 1 }], }), });
button.textContent = 'Added!';
// Refresh cart document.dispatchEvent(new CustomEvent('cart:updated'));
// Remove this item from cross-sells setTimeout(() => { button.closest('.cross-sell-item').remove(); }, 500); } catch (error) { button.textContent = 'Add'; button.disabled = false; } });});Smart Cross-Sells Based on Cart Contents
{%- liquid # Get product types in cart assign cart_types = cart.items | map: 'product' | map: 'type' | uniq
# Find complementary products assign cross_sells = '' | split: ','
for type in cart_types case type when 'T-Shirt' assign related = collections['pants'].products | slice: 0, 2 when 'Pants' assign related = collections['belts'].products | slice: 0, 2 when 'Shoes' assign related = collections['socks'].products | slice: 0, 2 endcase
if related assign cross_sells = cross_sells | concat: related endif endfor
# Remove products already in cart assign cart_product_ids = cart.items | map: 'product_id' assign filtered_cross_sells = cross_sells | where: 'available' | uniq-%}
{%- if filtered_cross_sells.size > 0 -%} <div class="cart-cross-sells"> <h3>Goes Great With</h3> {%- for product in filtered_cross_sells limit: 3 -%} {%- unless cart_product_ids contains product.id -%} {% render 'cart-cross-sell-item', product: product %} {%- endunless -%} {%- endfor -%} </div>{%- endif -%}Free Shipping Progress Bar
Encourage customers to reach free shipping threshold:
{# snippets/cart-shipping-bar.liquid #}
{%- assign free_shipping_threshold = settings.free_shipping_threshold | times: 100 -%}{%- assign cart_total = cart.total_price -%}{%- assign remaining = free_shipping_threshold | minus: cart_total -%}
<div class="shipping-bar"> {%- if remaining <= 0 -%} <p class="shipping-bar__message shipping-bar__message--success"> {% render 'icon-check' %} You've unlocked free shipping! </p> <div class="shipping-bar__progress shipping-bar__progress--complete"></div> {%- else -%} <p class="shipping-bar__message"> Add <strong>{{ remaining | money }}</strong> more for free shipping </p> {%- assign progress = cart_total | times: 100 | divided_by: free_shipping_threshold -%} <div class="shipping-bar__track"> <div class="shipping-bar__progress" style="width: {{ progress }}%;"></div> </div> {%- endif -%}</div>.shipping-bar { padding: var(--spacing-md); background: var(--color-background-secondary); border-radius: var(--border-radius); margin-bottom: var(--spacing-md);}
.shipping-bar__message { font-size: 0.875rem; text-align: center; margin: 0 0 var(--spacing-sm);}
.shipping-bar__message--success { display: flex; align-items: center; justify-content: center; gap: var(--spacing-xs); color: var(--color-success); font-weight: 500;}
.shipping-bar__track { height: 8px; background: var(--color-border); border-radius: 4px; overflow: hidden;}
.shipping-bar__progress { height: 100%; background: var(--color-primary); border-radius: 4px; transition: width 0.3s ease;}
.shipping-bar__progress--complete { width: 100%; background: var(--color-success);}Cross-Sell Carousel
For multiple suggestions:
<div class="cart-cross-sells"> <h3 class="cart-cross-sells__heading">You Might Also Like</h3>
<div class="cart-cross-sells__carousel" data-carousel> <button class="carousel__arrow carousel__arrow--prev" data-prev aria-label="Previous" disabled > {% render 'icon-chevron-left' %} </button>
<div class="carousel__track" data-track> {%- for product in cross_sell_products limit: 8 -%} <div class="carousel__slide"> {% render 'cart-cross-sell-item', product: product %} </div> {%- endfor -%} </div>
<button class="carousel__arrow carousel__arrow--next" data-next aria-label="Next" > {% render 'icon-chevron-right' %} </button> </div></div>Using Shopify Recommendations API
Fetch recommendations based on cart contents:
class CartRecommendations extends HTMLElement { async connectedCallback() { const cartItems = await fetch('/cart.js').then((r) => r.json());
if (cartItems.items.length === 0) return;
// Get recommendations for first cart item const productId = cartItems.items[0].product_id; const limit = this.dataset.limit || 4;
const response = await fetch( `/recommendations/products.json?product_id=${productId}&limit=${limit}&intent=complementary` );
const { products } = await response.json();
if (products.length === 0) { this.hidden = true; return; }
// Filter out products already in cart const cartProductIds = cartItems.items.map((item) => item.product_id); const filteredProducts = products.filter((p) => !cartProductIds.includes(p.id));
this.render(filteredProducts); }
render(products) { if (products.length === 0) { this.hidden = true; return; }
this.querySelector('[data-products]').innerHTML = products .map( (product) => ` <div class="cross-sell-item"> <a href="${product.url}"> <img src="${product.featured_image}?width=150" alt="${product.title}" width="75" height="75"> </a> <div class="cross-sell-item__info"> <a href="${product.url}" class="cross-sell-item__title">${product.title}</a> <p class="cross-sell-item__price">${this.formatMoney(product.price)}</p> </div> <button class="cross-sell-item__add button button--small" data-add-to-cart data-variant-id="${product.variants[0].id}" > Add </button> </div> ` ) .join('');
this.hidden = false; }
formatMoney(cents) { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format(cents / 100); }}
customElements.define('cart-recommendations', CartRecommendations);<cart-recommendations class="cart-cross-sells" data-limit="4" hidden> <h3 class="cart-cross-sells__heading">Frequently Bought Together</h3> <div class="cart-cross-sells__products" data-products> {# Populated via JavaScript #} </div></cart-recommendations>Cross-Sell Styles
.cart-cross-sells { padding: var(--spacing-md); border-top: 1px solid var(--color-border);}
.cart-cross-sells__heading { font-size: 0.9375rem; font-weight: 600; margin: 0 0 var(--spacing-md);}
.cart-cross-sells__products { display: flex; flex-direction: column; gap: var(--spacing-sm);}
.cross-sell-item { display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); background: var(--color-background-secondary); border-radius: var(--border-radius);}
.cross-sell-item__image img { width: 50px; height: 50px; object-fit: cover; border-radius: var(--border-radius);}
.cross-sell-item__info { flex: 1; min-width: 0;}
.cross-sell-item__title { display: block; font-size: 0.8125rem; font-weight: 500; color: inherit; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
.cross-sell-item__title:hover { text-decoration: underline;}
.cross-sell-item__price { font-size: 0.8125rem; color: var(--color-text-light); margin: 0;}
.cross-sell-item__add { flex-shrink: 0; padding: var(--spacing-xs) var(--spacing-sm); font-size: 0.75rem;}
.cross-sell-item__sold-out { font-size: 0.75rem; color: var(--color-text-light);}Complete Cart Drawer with Cross-Sells
{# sections/cart-drawer.liquid #}
<cart-drawer class="cart-drawer"> <div class="cart-drawer__overlay" data-cart-close></div>
<div class="cart-drawer__panel"> <div class="cart-drawer__header"> <h2>Your Cart ({{ cart.item_count }})</h2> <button data-cart-close aria-label="Close">×</button> </div>
{%- if cart.item_count > 0 -%} {# Free shipping bar #} {% render 'cart-shipping-bar' %}
{# Cart items #} <div class="cart-drawer__body"> {% render 'cart-drawer-items' %} </div>
{# Cross-sells #} <div class="cart-drawer__cross-sells"> {% render 'cart-cross-sells' %} </div>
{# Footer with totals #} <div class="cart-drawer__footer"> {% render 'cart-drawer-footer' %} </div> {%- else -%} {% render 'cart-drawer-empty' %} {%- endif -%} </div></cart-drawer>Practice Exercise
Implement cart cross-sells that:
- Show 3 complementary products
- Exclude products already in cart
- Have quick-add buttons
- Update cart without page reload
- Include a free shipping progress bar
Test by:
- Adding items and seeing relevant suggestions
- Quick-adding cross-sell items
- Verifying cart updates correctly
- Checking progress bar updates
Key Takeaways
- Filter out cart items from cross-sell suggestions
- Use complementary intent for related products
- Quick-add functionality reduces friction
- Free shipping bars incentivize larger orders
- Smart suggestions based on cart contents
- Refresh after adding to update cross-sells
- Keep it compact in the drawer format
- Test performance with API calls
Module Complete!
Congratulations on completing Module 11! You’ve mastered:
- Cart object fundamentals and line items
- Building cart drawers with AJAX updates
- Empty cart states with suggestions
- Quantity updates and item removal
- Notes, attributes, and discounts
- Cross-sells for increasing AOV
Next up: Module 12: Search and Predictive Search for building search functionality.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...