Display selected product variant’s associated image in shop

Somehow squarespace does not have this functionality out of the box. It inconsistently, or never displays the product image which is associated with the selected variant. For example, if I select a t-shirt color blue, the thumbnail will not change to the blue shirt. I wanted to remedy this, hence the code below. It still requires some tweaking, and you might want to add CSS for smoother transitions, but it works both on the product page, and on the category page if you followed my blog to enable carousel controls in shop category pages. The main function is filtering the images by the selected variant, subsequently hiding, or showing the images involved. The script involves normalization for different types of “x” characters and inch symbols, “cm”, etc. This ensures that sizes like “M”, or “XL” are filtered, while sizes like 13” and 15”, or 22”x30”, for example, are left to be compared against the image data. This means that a t-shirt size XL, which looks identical to a t-shirt size M on the picture, will not be filtered by size, leaving the filtering open for color options, while products such as pillows and cases, which vary in dimensions, will be size filtered. Theoretically there may be interference when the product has varying sizes and colors, I don’t have such products, so it is up to you to test.

HTML:

JAVASCRIPT:

<script>
document.addEventListener('DOMContentLoaded', function() {
const subString = 'shop';
const subStringTwo = 'shop/p/';
const getURL = window.location.href;
function onSelectChange(selectedOption, product){
var imageFound = false;
let currentThumbDiv = product.querySelector('.selected');
const allThumbnailDivs = product.querySelectorAll('.ProductItem-gallery-slides-item, .grid-image-wrapper');
const normalizedSelectedOption = selectedOption.trim().toLowerCase().replace(/\s+/g, '-').replace(/"/g, '').replace(/″/g, '').replace(/×/g, 'x');
allThumbnailDivs.forEach(thumbnailDiv => {
const allThumbnails = thumbnailDiv.querySelectorAll('img');
let currentThumb = thumbnailDiv.querySelector('.grid-image-cover');
allThumbnails.forEach(thumbnail => {
thumbnail.style.transition = 'opacity 0s ease !important';
thumbnail.classList.add('loaded');
thumbnail.style.opacity = '1';
thumbnail.classList.remove('grid-image-hover');
if (getURL.includes(subStringTwo)) {
const altText = thumbnail.getAttribute('alt') || '';
const normalizedAltText = altText.trim().toLowerCase().replace(/[\s-_]+/g, '-').replace(/×/g, 'x');
if (normalizedAltText.includes(normalizedSelectedOption)) {
if (!imageFound){
imageFound = true;
thumbnailDiv.classList.add('selected');
currentThumbDiv = thumbnailDiv;
}
} else {
thumbnailDiv.classList.remove('selected');
currentThumbDiv.classList.add('selected');
}
} else {
let srcText = thumbnail.getAttribute('src') || '';
srcText = srcText.substring(srcText.lastIndexOf('/') + 1);
srcText = srcText.replace(/\.[^/.]+$/, '');
const normalizedSrcText = srcText.trim().toLowerCase().replace(/[\s-_]+/g, '-').replace(/×/g, 'x');
  console.log("Comparing selected option: ", normalizedSelectedOption, " with src text: ", normalizedSrcText);
if (normalizedSrcText.includes(normalizedSelectedOption)) {
if (!imageFound){
imageFound = true;
thumbnail.style.display = 'block';
thumbnail.style.opacity = '1';
thumbnail.classList.add('loaded');
thumbnail.classList.add('active');
thumbnail.classList.add('grid-image-selected');
currentThumb = thumbnail;
}
} else {
thumbnail.classList.remove('loaded');
thumbnail.classList.remove('grid-image-selected');
thumbnail.classList.remove('active');
thumbnail.style.display = 'none';
thumbnail.style.opacity = '0';
if (currentThumb){
currentThumb.style.display = 'block';
currentThumb.classList.add('loaded');
currentThumb.classList.add('active');
thumbnail.classList.add('grid-image-selected');
currentThumb.style.opacity = '1';
}
}
}
});
});
}
function addSelectEventListeners(product) {
let productSelects = product.querySelectorAll('select');
productSelects = Array.from(productSelects).filter(select => {
const optionName = select.getAttribute('data-variant-option-name') || '';
if (optionName === 'Size' || optionName === 'Select Size') {
const options = Array.from(select.options).map(option => option.value);
return options.some(option => option.includes('″') || option.includes('"') || option.includes('cm'));
}
return true;
});
productSelects.forEach(select => {
select.addEventListener('change', function(event) {
const selectedOption = event.target.value;
onSelectChange(selectedOption, product);
});
});
}
function observeProducts() {
const productContainer = document.querySelector('.list-grid');
if (productContainer) {
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1 && node.matches('.grid-item, .ProductItem')) {
addSelectEventListeners(node);
}
});
});
});
observer.observe(productContainer, {
childList: true,
subtree: true
});
document.querySelectorAll('.grid-item, .ProductItem').forEach(addSelectEventListeners);
}
}
if (getURL.includes(subString)) {
observeProducts();
}
});
</script>
CSS:

Update: I added a mutation observer, because the product grid loads in the products dynamically.

Previous
Previous

Display number of visitors on each product page With Firebase

Next
Next

Automatic form filling and dynamic product image