Cart Page and Cart Drawer Intermediate 10 min read

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

  1. Complementary products: Items that go with cart contents
  2. Frequently bought together: Based on purchase history
  3. Threshold incentives: “Add $X more for free shipping”
  4. Bundle suggestions: Complete the set
  5. 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);
}

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:

  1. Show 3 complementary products
  2. Exclude products already in cart
  3. Have quick-add buttons
  4. Update cart without page reload
  5. 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

  1. Filter out cart items from cross-sell suggestions
  2. Use complementary intent for related products
  3. Quick-add functionality reduces friction
  4. Free shipping bars incentivize larger orders
  5. Smart suggestions based on cart contents
  6. Refresh after adding to update cross-sells
  7. Keep it compact in the drawer format
  8. 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...