Example : Custom Images Logic Based On Product Attributes
This script adds a hover image effect to products in “Discover” collections — showing an alternate image when hovering, and updating images dynamically when users click or hover on color swatches.
JS Code:
var __fast_options = __fast_options || {};
__fast_options.with_product_attributes = true;
/* Utility: get all product cards */
function getProductElements(element) {
return [...element.querySelectorAll(`.fs-results-product-card`)];
}
/* Utility: extract the secondary image (prefers ones containing “flat”) */
function getSecondProductImage(attributes) {
for (const [key, value] of attributes) {
if (key === "Product images" && Array.isArray(value) && value.length > 0) {
const flatImage = value.find(url => url.toLowerCase().includes("flat"));
if (flatImage) return flatImage;
if (value.length >= 3) return value[2];
return null;
}
}
return null;
}
/* Main logic hook */
function hooks() {
SerpOptions.registerHook('serp-product-grid', ({ products, element }) => {
for (const productElement of getProductElements(element)) {
const productID = productElement.dataset.productId;
const data = products[productID];
const attributes = data?.attributes;
// Only for “Discover” collections
if (window.location.pathname.toLowerCase().includes('/collections/discover') && attributes) {
const imageContainer = productElement.querySelector('.image-container');
if (!imageContainer) continue;
const second = getSecondProductImage(attributes);
const originalImg = imageContainer.querySelector('img:not(.cloned)');
const imageWrapper = productElement.querySelector('.image-container .wrapper');
if (imageWrapper && second && originalImg && !imageContainer.classList.contains("cstm")) {
// Clone images
const firstImageClone = originalImg.cloneNode(true);
const secondImageClone = originalImg.cloneNode(true);
firstImageClone.classList.add("cloned", "first");
firstImageClone.src = second;
secondImageClone.classList.add("cloned", "second");
// Hide original
originalImg.style.display = "none";
// Append clones
imageWrapper.appendChild(firstImageClone);
imageWrapper.appendChild(secondImageClone);
// Hover swap
imageContainer.addEventListener('mouseleave', () => {
firstImageClone.style.display = "flex";
secondImageClone.style.display = "none";
});
imageContainer.addEventListener('mouseenter', () => {
firstImageClone.style.display = "none";
secondImageClone.style.display = "flex";
});
// Mark processed
imageContainer.classList.add('cstm');
}
// Update images when color swatches are hovered or clicked
productElement.querySelectorAll(".color-swatch:not(.cstm-event)").forEach((swatch) => {
swatch.classList.add("cstm-event");
const colorContainer = swatch.closest(".color-swatch-container");
const colorName = colorContainer.querySelector(".color-swatch-color-name")?.innerHTML;
if (colorName) {
const swatchProductData = data.alternativeProducts.find(
altProduct => altProduct["color"].trim().toLowerCase() == colorName.trim().toLowerCase()
);
if (swatchProductData && originalImg) {
swatch.addEventListener('click', () => {
const secondImage = getSecondProductImage(swatchProductData.attributes);
const firstImage = originalImg.src;
if (secondImage && firstImage) {
imageContainer.querySelector('img.cloned.first').src = secondImage;
imageContainer.querySelector('img.cloned.second').src = firstImage;
}
});
colorContainer.addEventListener('mouseover', () => {
swatch.click();
});
}
}
});
}
}
});
}
/* Execute after Fast Simon SERP is ready */
if (window.SerpOptions) {
hooks();
} else {
window.addEventListener('fast-serp-ready', function () {
hooks();
});
}
notes
- the example handles alternative products but possible on "regular" color variants with data adjustments