Add Dynamic Upsell Products in Your Shopify Store
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
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>
<!-- 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' %}
{% 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>
Add upsell products in shopify admin in Upsell Products field in product edit or add.
Predien