Providing customers with the ability to select product variants directly from the collection page is a great way to enhance the shopping experience and streamline their journey. Rather than navigating to individual product pages to view options like size or color, a variant selector allows customers to quickly browse and choose the exact product variant they want right from the collection view. In this blog, we’ll walk you through how to add a variant selector to your Shopify store’s collection page, making it easier for shoppers to find and select the products they love in less time.
Code: https://github.com/ndrishinski/blogs/commit/a5a2450b0ae4d681dab94b93057efac4ebb62886
Create a New Section
The first thing we want to do, is create a new section file. I am going to name mine “new-collection-list.liquid” but you can name yours whatever you would like. After creating this file under your sections directory we can paste the following styles at the top of the file:
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap');
.custom-collection {
gap: 30px;
display: flex;
justify-content: center;
}
.product {
background-color: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
position: relative;
min-width: 320px;
}
.product:hover {
transform: translateY(-10px);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2);
}
.product-image-container {
position: relative;
overflow: hidden;
height: 300px;
}
.product-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.product:hover .product-image {
transform: scale(1.1);
}
.product-info {
padding: 20px;
position: relative;
cursor: pointer;
}
.product-title {
margin: 0 0 10px;
font-size: 20px;
font-weight: 600;
cursor: pointer;
}
a.product-link {
text-decoration: none;
cursor: pointer;
color: black;
font: inherit;
}
a.product-link:hover {
text-decoration: underline;
}
.product-price {
font-weight: 300;
font-size: 24px;
color: #4a4a4a;
margin-bottom: 15px;
}
.color-options {
display: flex;
gap: 12px;
margin-top: 15px;
}
.color-option {
width: 25px;
height: 25px;
border-radius: 50%;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.3s ease;
position: relative;
display: block !important;
border: 1px solid #000;
}
.size-options {
gap: 2px;
}
.color-option::after {
content: '';
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border-radius: 50%;
border: 2px solid transparent;
transition: all 0.3s ease;
}
.color-option.active::after {
border-color: #333;
transform: scale(1.2);
}
.hide {
display: none;
}
.add-to-cart {
position: absolute;
bottom: 20px;
right: 20px;
background-color: #333;
color: white;
border: none;
padding: 10px 20px;
border-radius: 25px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
opacity: 0;
transform: translateY(20px);
}
.product:hover .add-to-cart {
opacity: 1;
transform: translateY(0);
}
.add-to-cart:hover {
background-color: #555;
}
@media (max-width: 768px) {
body {
padding: 20px;
}
.custom-collection {
gap: 20px;
flex-direction: column;
}
.product-image-container {
height: 250px;
}
}
</style>
Note this is not the preferred way of adding a google font, but for this tutorial it serves its purpose. I am not going to talk at length about this stylesheet but rather will focus on the HTML and JavaScript code.
Next we can add our Liquid/HTML code below our styles:
<div class="custom-collection" id="productCollection"></div>
I know your thinking what?? How is that everything. Well the secret here, is that this is just our DOM element that we will insert all of our other dynamic content into. It will serve as our “container” so to speak.
Now we can focus on adding our JavaScript:
<script>
let storeBackgroundColors = {}
// can only retrieve metafields via liquid, so we'll push them into this JS object
{% assign liquiProducts = section.settings.collection.products %}
{% for prod in liquiProducts %}
{% for variant in prod.variants %}
storeBackgroundColors[{{ variant.id }}] = "{{ variant.metafields.custom.variant_color.value }}"
{% endfor %}
{% endfor %}
const originalProducts = {{ section.settings.collection.products | json }}.slice(0, {{ section.settings.product-limit }})
let allProds = originalProducts.map(prod => {
let temporaryStorage = {}
let arrayForColors = []
let arrayForSizes = []
prod.variants.forEach(vari => {
if (vari.option1 != 'Default Title') {
if (!temporaryStorage[vari.option1]) {
temporaryStorage[vari.option1] = { ...vari }
arrayForColors.push(vari)
}
if (!temporaryStorage[vari.option2]) {
temporaryStorage[vari.option2] = { ...vari }
arrayForSizes.push(vari.option2)
}
}
})
return { ...prod, variants: arrayForColors, sizes: arrayForSizes }
})
const products = allProds
function createProductElement(product) {
const productElement = document.createElement('div');
productElement.className = 'product';
productElement.innerHTML = `
<div class="product-image-container">
<img src="${product.variants.length > 1 ? product.variants[0].featured_image.src : product.images[0]}" alt="${product.title}" class="product-image">
</div>
<div class="product-info" data-product-handle="${product.handle}">
<a class="product-link" href="/products/${product.handle}"<h3class="product-title">${product.title}</h3></a>
<p class="product-price">$${product.price.toFixed(2)}</p>
<div class="color-options ${product.variants.length <= 1 ? 'hide' : ''}">
${product.variants.length && product.variants.map((variant, index) => `
<div class="${variant.option1 === 'Default Title' ? 'hide' : 'color-option'} ${index === 0 ? 'active' : ''}"
style="background-color: ${storeBackgroundColors[variant.id]};"
data-color-index="${index}"
data-variant-id="${variant.id}"
></div>
`).join('')}
</div>
<div class="color-options size-options ${product.sizes.length <= 1 ? 'hide' : ''}">
${product.sizes.length && product.sizes.map((size, index) => `
<div style="background-color: #f1f1f2; display: flex; justify-content: center; align-items: center; height: 22px; border-radius: 10px;">
<p style="font-size: 14px; padding: 0 6px;">${size}</p>
</div>
`).join('')}
</div>
<button class="add-to-cart">Add to Cart</button>
</div>
`;
function addToCart(data) {
let formData = {
'items': [{
'id': data,
'quantity': 1
}]
};
fetch(window.Shopify.routes.root + 'cart/add.js', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => {
window.location.href = '/cart'
return response.json();
})
.catch((error) => {
console.error('Error:', error);
});
}
document.querySelectorAll('.add-to-cart').forEach(addToCartButton => {
addToCartButton.addEventListener('click', async () => {
const colorOptions = addToCartButton.parentNode.querySelector('.color-options');
const activeColorOption = colorOptions.querySelector('.active');
let variantId = activeColorOption.dataset.variantId;
addToCart(variantId);
});
});
const colorOptions = productElement.querySelectorAll('.color-option');
const productImage = productElement.querySelector('.product-image');
colorOptions.forEach(option => {
option.addEventListener('click', () => {
const colorIndex = option.getAttribute('data-color-index');
productImage.src = product.variants[colorIndex].featured_image.src
colorOptions.forEach(opt => opt.classList.remove('active'));
option.classList.add('active');
});
});
return productElement;
}
function renderProducts() {
const collectionElement = document.getElementById('productCollection');
products.forEach(product => {
const productElement = createProductElement(product);
collectionElement.appendChild(productElement);
});
}
renderProducts();
</script>
Wow that’s a lot. What is going on here?
Well first we are using liquid to retrieve our metafields (that we will create in the next step). An important thing to remember (as I forgot) is that you can only retrieveAf metafields via liquid code. You can’t retrieve these values via JavaScript! So we are storing the metafield for each variant in a JavaScript object that we will reference later.
After storing our metafield values for the swatch color, we separate our variants based on the option1 and option2 value. If you have no idea what those are, suffice it to say that when you create variants with color and sizes, these will be stored as option1 and option2 (for this tutorial make sure that color is the first).
Then what we do is create the HTMl that will display our color swatches and then insert them into the DOM. We also add event listeners to add the variant to cart and to listen for a click to switch the variant colors!
Now we can our simple schema below our JavaScript:
{% schema %}
{
"name": "New Collection List",
"class": "section",
"tag": "section",
"settings": [
{
"type": "collection",
"id": "collection",
"label": "t:sections.featured-collection.settings.collection.label"
},
{
"type": "number",
"id": "product-limit",
"label": "# of products to show",
"default": 3
}
],
"presets": [
{
"name": "New Collection List",
"settings": {},
"blocks": []
}
]
}
{% endschema %}
Now you can add as many customizations as you would like here, but for now we are simply letting the merchant choose the number of products they would like to display in this section.
Creating a Variant Metafield
Now that our code is all in place, we can create our variant metafield. Go to Settings -> Custom Data -> Variants -> Add defintion.
Then you can give your metafield a name. For mine, I called it Variant Color and it looks like so:
If you name yours something different, make sure you update the code which references our metafield because it currently references metafields.custom.variant_color.value! But you can swap it to whatever you named your metafield.
Add our Section to the Customizer
Now that our code is in place and our metafield is created, we can navigate to the customizer and add our section to any template of our choosing.
Conclusion
Adding a variant selector to your Shopify collection page not only boosts user experience but can also improve conversion rates by making it faster and easier for customers to find and choose their preferred options. This streamlined approach keeps customers engaged while browsing and encourages quicker decision-making. With these steps, you’re now set to customize your store in a way that meets your customers’ needs and enhances their overall shopping journey.