Add Dynamic Upsell Products in Your Shopify Store

08 Nov 2025

Add Dynamic Upsell Products in Your Shopify Store

1. Create a metafields

Create a metafields in products named "Upsell Products" and select type is product and next select List of products . So the key may be "custom.upsell_products" then save it.

settings > Metafields and metaobjects > Products 

2. Create a section for products list

Create "product-grid.liquid" and add this code.

{% schema %}

{

"name": "My Product GRID",

"settings": [

{

"type": "collection",

"id": "collection_products",

"label": "Collection"

},

{

"type": "text",

"id": "crt_button_text",

"label": "Button text",

"default": "Add to cart"

},

{

"type": "text",

"id": "crt_button_border_radius",

"label": "button_border_radius",

"default": "12"

},

{

"type": "text",

"id": "crt_button_text_size",

"label": "Button text size",

"default": "12"

},

{

"type": "select",

"id": "crt_btn_align",

"label": "ms_btn_align",

"options": [

{

"label": "Start",

"value": "left"

},

{

"label": "Center",

"value": "center"

},

{

"label": "End",

"value": "right"

}

]

},

{

"type": "text",

"id": "crt_button_horizontal_padding",

"label": "button_horizontal_padding",

"default": "30"

},

{

"type": "text",

"id": "crt_button_vertical_padding",

"label": "button_vertical_padding",

"default": "15"

},

{

"type": "select",

"id": "crt_button_font_weight",

"label": "Button font weight",

"options": [

{

"label": "Bold",

"value": "bold"

},

{

"label": "Normal",

"value": "500"

}

]

},

{

"type": "color",

"id": "crt_hero_button_color",

"label": "Button Color",

"default": "#FFFFFF"

},

{

"type": "color",

"id": "crt_hero_button_hover_color",

"label": "hero-button-hover-color",

"default": "#D6D6D6"

},

{

"type": "color",

"id": "crt_hero_button_text_color",

"label": "hero-button-text-color",

"default": "#000000"

},

{

"type": "color",

"id": "crt_hero_button_text_hover_color",

"label": "hero-button-text-hover-color",

"default": "#000000"

}

],

"presets": [

{

"name": "My Product GRID"

}

]

}

{% endschema %}

{% liquid

assign collection = section.settings.collection_products

%}

{% render 'cart-drawer-2' %}

<section style="width: 100%; padding: 0; margin: auto;" class="page-width">

<div class="product-list my-12">

{% for product in collection.products limit: 4 %}

<div class="product-item" style="display: flex; flex-direction: column; position: relative;" class="relative">

<a href="{{ product.url }}" style="text-decoration: none; color: inherit;">

<div class="product_images">

<img

src="{{ product.images[0] | img_url: 'master' }}"

{% if product.images.size > 1 %}

data-hover-src="{{ product.images[1] | img_url: 'master' }}"

{% endif %}

alt="{{ product.title }}"

height="280px"

width="350px"

style="border-radius: 12px; object-fit: cover; height: 280px; width: 350px;"

class="hover-image"

>

</div>

<div class="mt-6">

<h4>{{ product.title }}</h4>

<div>{{ product.price | money }}</div>

</div>

</a>

<div class="p-[10px] text-white rounded-xl mt-16 flex justify-center w-full z-0"></div>

{% if product.available %}

<div class="mt-8 flex justify-{{ section.settings.crt_btn_align }}">

<button

command="show-modal"

commandfor="drawer"

class="add_to_cart_btn gradient duration-150"

style="font-size: {{ section.settings.crt_button_text_size }}px; background-color: {{ section.settings.crt_hero_button_color}}; color: {{ section.settings.crt_hero_button_text_color }}"

data-product-id="{{ product.variants.first.id }}"

data-upsells='{{ product.metafields.custom.upsell_products.value | json }}'

onclick="addToCartFromButton(this)"

>

{{- section.settings.crt_button_text -}}

</button>

</div>

{% else %}

<button class="bg-gray-500 p-[10px] text-white rounded-xl mt-6 flex justify-center w-full" disabled>

Sold Out

</button>

{% endif %}

</div>

{% endfor %}

</div>

</section>

<script>

document.addEventListener('DOMContentLoaded', function () {

const images = document.querySelectorAll('.hover-image');

images.forEach((img) => {

if (img.dataset.hoverSrc) {

const originalSrc = img.src;

const hoverSrc = img.dataset.hoverSrc;

img.addEventListener('mouseover', () => {

img.src = hoverSrc;

});

img.addEventListener('mouseout', () => {

img.src = originalSrc;

});

}

});

});

function addToCartFromButton(button) {

const id = button.dataset.productId;

const upsells = button.dataset.upsells;

addToCartButtons(event, id, upsells);

}

const addToCartButtons = (event, id, upsellProducts) => {

event.stopPropagation();

console.log('Adding product to cart:', id);

fetch('cart/add.js', {

method: 'POST',

headers: {

'Content-Type': 'application/json',

},

body: JSON.stringify({

id: id,

quantity: 1,

}),

})

.then((response) => response.json())

.then((data) => {

updateCart();

updateCartCount();

if(upsellProducts){

const parsedProducts = JSON.parse(upsellProducts)

upsellContainer.innerHTML = ``

parsedProducts.forEach((product) => {

const productCard = createUpsellCard(product);

upsellContainer.appendChild(productCard);

});

localStorage.setItem('upsellProducts', JSON.stringify(upsellProducts));

}

})

.catch((error) => {

console.error('Error adding product to cart:', error);

});

};

</script>

<style>

.product-list {

display: grid;

grid-template-columns: 1fr 1fr 1fr 1fr;

gap: 20px;

}

@media (max-width: 750px) {

.product-list {

grid-template-columns: 1fr 1fr;

}

}

.product_images {

display: flex;

overflow: hidden;

overflow: auto;

height: 300px;

}

.product-item {

border: 1px solid rgb(216, 216, 216);

border-radius: 12px;

max-width: 350px;

padding: 12px;

}

.add_to_cart_btn {

padding: {{ section.settings.crt_button_vertical_padding }}px {{ section.settings.crt_button_horizontal_padding }}px;

text-decoration: none;

font-weight: {{ section.settings.crt_button_font_weight }} !important;

border-radius: {{ section.settings.crt_button_border_radius }}px;

}

.add_to_cart_btn:hover {

background-color: {{ section.settings.crt_hero_button_hover_color }} !important;

color: {{ section.settings.crt_hero_button_text_hover_color }} !important;

}

@media screen and (min-width: 750px) {

.list-menu__item--link {

padding-bottom: 0.5rem;

padding-top: 0.5rem;

}

}

</style>

3. Create 'cart-drawer-2.liquid"

<!-- Include TailwindPlus Elements (required for `el-dialog`) -->

<script src="https://cdn.jsdelivr.net/npm/@tailwindplus/elements@1" type="module"></script>

<!-- Drawer Component -->

<el-dialog>

<dialog

id="drawer"

aria-labelledby="drawer-title"

class="fixed inset-0 size-auto max-h-none max-w-none overflow-hidden bg-transparent not-open:hidden backdrop:bg-transparent"

>

<!-- Backdrop with click-to-close enabled -->

<el-dialog-backdrop

command="close"

commandfor="drawer"

class="absolute inset-0 bg-gray-500/75 transition-opacity duration-200 ease-in-out data-closed:opacity-0"

></el-dialog-backdrop>

<div

tabindex="0"

class="absolute inset-0 pl-10 focus:outline-none sm:pl-16"

>

<el-dialog-panel

class="group/dialog-panel relative ml-auto block size-full max-w-3xl transform transition duration-200 ease-in-out data-closed:translate-x-full sm:duration-200"

>

<!-- Drawer Content -->

<div class="flex h-full flex-col overflow-y-auto bg-white py-6 shadow-xl">

<div class="px-4 sm:px-6 relative flex justify-between">

<h2

id="drawer-title"

class="text-base font-semibold text-gray-900"

>

Cart

</h2>

<button

command="close"

commandfor="drawer"

class="mr-12 hover:scale-[1.2] hover:stroke-black"

>

<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 24 24" fill="none">

<path d="M20 20L4 4.00003M20 4L4.00002 20" stroke="gray" stroke-width="2" stroke-linecap="round"/>

</svg>

</button>

</div>

<div id="cart-items" class="relative mt-6 flex flex-col px-4 sm:px-6">

<!-- Your custom content goes here -->

</div>

<div class="absolute bottom-[150px] left-0 right-0">

<h3 class="px-6 mb-2 text-[24px] font-bold">Recommended Products</h3>

<div id="upsell_products_container" class="flex flex-col scrollbar-hide px-6"></div>

</div>

<!-- Checkout button -->

<div id="checkout-close-button"></div>

</div>

</el-dialog-panel>

</div>

</dialog>

</el-dialog>

<!-- Optional: TailwindCSS Browser Plugin -->

<script async src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

<script>

const upsellContainer = document.getElementById('upsell_products_container');

function updateCart() {

fetch('/cart.js')

.then((response) => response.json())

.then((cart) => {

const cartItemsContainer = document.getElementById('cart-items');

const cartItemsCheckout = document.getElementById('checkout-close-button');

// upsell script

const upsell = document.getElementById('upsell_products');

cartItemsContainer.innerHTML = ''; // Clear previous content

if (cart.items.length === 0) {

cartItemsContainer.innerHTML = '<p>Your cart is empty.</p>';

return;

} else {

cartItemsCheckout.innerHTML = `

<div class="absolute bottom-0 left-0 right-0 px-4 py-4 mb-6 sm:px-6 w-full">

<a

href="/checkout"

command="close"

commandfor="drawer"

class="w-full flex justify-center rounded-xl bg-gray-900 py-3 text-md font-semibold text-white hover:bg-gray-800 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-white"

>

Checkout

</a>

<button

command="close"

commandfor="drawer"

class="w-full mt-4 flex justify-center rounded-xl bg-gray-200 py-3 text-md font-semibold text-gray-900 hover:bg-gray-300 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-gray-500"

>

Close

</button>

</div>

`;

}

const totalQtyDiv = document.createElement('div');

totalQtyDiv.innerHTML = `

<div class="mb-12 flex items-center justify-between">

<div>

Total Quantity: <span class="text-gray-900 font-bold">${cart.item_count}</span>

</div>

<div>

Total Price: <span class="text-gray-900 font-bold">${cart.total_price / 100} tk</span>

</div>

</div>

`;

cartItemsContainer.appendChild(totalQtyDiv);

cart.items.forEach((item) => {

const itemElement = document.createElement('div');

// Show total quantity only once, not inside each item

itemElement.className = 'flex items-center justify-between border-b border-gray-300 pb-2';

itemElement.innerHTML = `

<div class="flex items-center justify-between w-full mt-6">

<div class="flex items-start space-x-4">

<img src="${item.image}" alt="${item.product_title}" class="h-[100px] w-[100px] object-cover rounded" />

<div>

<h3 class="text-md font-bold text-gray-900">${item.product_title}</h3>

<div>

<span class="text-gray-900 font-bold">${(item.price / 100).toFixed(2)} tk</span>

<span class="text-gray-600"> x ${item.quantity}</span>

<span class="text-gray-600"> = ${((item.price * item.quantity) / 100).toFixed(2)} tk</span>

</div>

<p

class="mt-4 text-md text-gray-600"

>

<div class="flex items-center space-x-2 mt-5">

<button onclick="decreaseQuantity(event, ${item.id}, ${

item.quantity

})" class="min-h-[20px] min-w-[25px] rounded-xl bg-gray-100 hover:bg-gray-200 duration-200">-</button>

<input

type='number'

value=${item.quantity}

onchange="onChangeQuantity(event.target.value, ${item.id})"

class="w-[70px] px-2 mx-4 text-center border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-gray-500"

/>

<button onclick="increaseQuantity(event, ${item.id}, ${

item.quantity

})" class="min-h-[20px] min-w-[25px] rounded-xl bg-gray-100 hover:bg-gray-200 duration-200">+</button>

</div>

</p>

</div>

</div>

<button>

<span class="text-red-500 hover:text-red-700 hover:underline" onclick="removeItemFromCart('${

item.id

}')">Remove</span>

</button>

</div>

`;

cartItemsContainer.appendChild(itemElement);

});

})

.catch((error) => {

console.error('Failed to load cart:', error);

});

}

updateCart();

const removeItemFromCart = (itemId) => {

fetch(`/cart/change.js`, {

method: 'POST',

headers: {

'Content-Type': 'application/json',

},

body: JSON.stringify({ id: itemId, quantity: 0 }),

})

.then((response) => response.json())

.then(() => {

updateCart(); // Refresh the cart after removing an item

updateCartCount();

{% comment %} localStorage.removeItem("upsellProducts");

upsellContainer.innerHTML= `` {% endcomment %}

})

.catch((error) => {

console.error('Failed to remove item from cart:', error);

});

};

const onChangeQuantity = (quantity, id) => {

if (quantity === 0) {

removeItemFromCart(id);

return;

}

fetch('/cart/change.js', {

method: 'POST',

headers: {

'Content-Type': 'application/json',

},

body: JSON.stringify({

id: id.toString(), // Ensure id is a string

quantity: parseInt(quantity, 10),

}),

})

.then((response) => response.json())

.then(() => {

updateCart(); // Refresh the cart after changing quantity

updateCartCount();

})

.catch((error) => {

console.error('Failed to change item quantity:', error);

});

};

const increaseQuantity = (event, id, quantity) => {

event.stopPropagation();

const currentQuantity = quantity + 1;

onChangeQuantity(currentQuantity, id);

updateCartCount();

};

const decreaseQuantity = (event, id, quantity) => {

event.stopPropagation();

if (quantity > 1) {

const currentQuantity = quantity - 1;

onChangeQuantity(currentQuantity, id);

updateCartCount();

}

};

// upsell script

const upsellProducts = JSON.parse(localStorage.getItem('upsellProducts'));

// Helper function to create an upsell product card

const createUpsellCard = (product) => {

const card = document.createElement('div');

card.className = `

bg-white shadow-md rounded-lg border border-gray-200

hover:shadow-lg transition-shadow

flex flex-row justify-between items-center gap-4

p-4 w-full mt-3

`;

card.innerHTML = `

<!-- Product Image -->

<div class="flex gap-6">

<div class="w-20 h-20 flex-shrink-0">

<img

src="${product.images[0] || 'https://via.placeholder.com/300'}"

alt="${product.title}"

class="w-full h-full object-cover rounded-md"

/>

</div>

<!-- Product Info -->

<div class="flex-1 text-left overflow-hidden max-w-[280px] line-clamp-1">

<h3 class="text-lg font-semibold text-gray-800 truncate">

${product.title}

</h3>

<p class="mt-1 text-md font-bold text-gray-900">

$${product.price?.toFixed(2) || '0.00'}

</p>

</div>

</div>

<!-- Add Button -->

<button

class="border border-gray-700 text-[16px] w-10 h-10 rounded-full

flex items-center justify-center hover:bg-black hover:text-white cursor-pointer

transition duration-200 focus:outline-none focus:ring-2 focus:ring-black"

onclick="addToCartButtons(event, ${product.variants[0].id})"

>

+

</button>

`;

return card;

};

if (Array.isArray(upsellProducts) && upsellProducts.length > 0) {

upsellContainer.innerHTML= ``

upsellProducts.forEach((product) => {

const productCard = createUpsellCard(product);

upsellContainer.appendChild(productCard);

});

} else {

upsellContainer.innerHTML = ``;

}

</script>

add to header using this code

{% render 'cart-drawer-2' %}

4. Create a custom liquid for "add to cart" button in product page

{% if product %}

  <div class="my-8  flex" style="margin: 10px 0 15px 0">

    <button

      command="show-modal"

      commandfor="drawer"

      onclick="addToCartButtons(event, '{{ product.variants.first.id }}')"

      class="add_to_cart_btn gradient duration-150 "

      style="font-size: 16px;  color: black; padding: 10px 20px; border-radius: 7px; border: 1px solid gray; width: 100%"

      onmouseover="this.style.backgroundColor='gray'; this.style.color='white';"

      onmouseout="this.style.backgroundColor='white'; this.style.color='black';"

    >

      Add To Cart

    </button>

  </div>

{% else %}

  <button class="bg-gray-500 p-[10px] text-white rounded-xl mt-6 flex justify-center w-full" disabled>Sold Out</button>

{% endif %}

<style>

  .add_to_cart_btn:hover {

    background-color: gray;

  }

</style>

<script>

  const addToCartButtons = (event, id) => {

    event.stopPropagation();

    console.log('Adding product to cart:', id);

    fetch('https://humaira-haven.myshopify.com/cart/add.js', {

      method: 'POST',

      headers: {

        'Content-Type': 'application/json',

      },

      body: JSON.stringify({

        id: id,

        quantity: 1,

      }),

    })

      .then((response) => response.json())

      .then((data) => {

        updateCart();

        updateCartCount();

        console.log('Product added to cart:', data);

        const upsellProducts = {{ product.metafields.custom.upsell_products.value | json }}

        if (upsellProducts) {

          upsellContainer.innerHTML = ``

          upsellProducts.forEach((product) => {

              const productCard = createUpsellCard(product);

              upsellContainer.appendChild(productCard);

          });

          localStorage.setItem('upsellProducts', JSON.stringify(upsellProducts));

          console.log('Upsell products stored in localStorage:', upsellProducts);

          window.dispatchEvent(new Event("upsell"))

        } else {

            console.log('No upsell products found.');

        }

      })

      .catch((error) => {

        console.error('Error adding product to cart:', error);

      });

  };

</script>

5. Add upsell products

Add upsell products in shopify admin in Upsell Products field in product edit or add.

predien software agencyPredien

Customized software solutions, cutting-edge innovation, and data-driven intelligence

Connect with us

Copyright @ 2025 Predien
Crafted by Mariful