@kpsclothing Let me take you through the steps. There are three steps watch out for it.
////// First step /////
First give your product images alt text from the product admin.
So for a white product write white and for a red red. Just make sure the name of your color variants should be exactly same as this alt text like image below

hope you get the point.
////////// Second step ////////
Open your product-media-gallery.liquid file in your code editor and replace this code.
{% comment %}
Renders a product media gallery. Should be used with âmedia-gallery.jsâ
Also see âproduct-media-modalâ
Accepts:
- product: {Object} Product liquid object
- variant_images: {Array} Product images associated with a variant
- limit: {Number} (optional) When passed, limits the number of media items to render
Usage:
{% render âproduct-media-galleryâ %}
{% endcomment %}
{%- liquid
if section.settings.hide_variants and variant_images.size == product.media.size
assign single_media_visible = true
endif
if limit == 1
assign single_media_visible = true
endif
assign media_count = product.media.size
if section.settings.hide_variants and media_count > 1 and variant_images.size > 0
assign media_count = media_count | minus: variant_images.size | plus: 1
endif
if media_count == 1 or single_media_visible
assign single_media_visible_mobile = true
endif
if media_count == 0 or single_media_visible_mobile or section.settings.mobile_thumbnails == âshowâ or section.settings.mobile_thumbnails == âcolumnsâ and media_count < 3
assign hide_mobile_slider = true
endif
if section.settings.media_size == âlargeâ
assign media_width = 0.65
elsif section.settings.media_size == âmediumâ
assign media_width = 0.55
elsif section.settings.media_size == âsmallâ
assign media_width = 0.45
endif
-%}
<media-gallery
id=âMediaGallery-{{ section.id }}â
role=âregionâ
{% if section.settings.enable_sticky_info %}
class=âproduct__column-stickyâ
{% endif %}
aria-label=â{{ âproducts.product.media.gallery_viewerâ | t }}â
data-desktop-layout=â{{ section.settings.gallery_layout }}â
{{ 'accessibility.skip_to_product_info' | t }}
{% render 'icon-caret' %}
1
/
{{ 'general.slider.of' | t }}
{{ media_count }}
{% render 'icon-caret' %}
{%- if first_3d_model -%}
{% render 'icon-3d-model' %}
{{ 'products.product.xr_button' | t }}
{%- endif -%}
{%- liquid
assign is_not_limited_to_single_item = false
if limit == null or limit > 1
assign is_not_limited_to_single_item = true
endif
-%}
{%- if is_not_limited_to_single_item
and media_count > 1
and section.settings.gallery_layout contains 'thumbnail'
or section.settings.mobile_thumbnails == 'show'
-%}
{% render 'icon-caret' %}
{%- capture sizes -%}
(min-width: {{ settings.page_width }}px) calc(({{ settings.page_width | minus: 100 | times: media_width | round }} - 4rem) / 4),
(min-width: 990px) calc(({{ media_width | times: 100 }}vw - 4rem) / 4),
(min-width: 750px) calc((100vw - 15rem) / 8),
calc((100vw - 8rem) / 3)
{%- endcapture -%}
{%- if featured_media != null -%}
{%- liquid
capture media_index
if featured_media.media_type == âmodelâ
increment model_index
elsif featured_media.media_type == âvideoâ or featured_media.media_type == âexternal_videoâ
increment video_index
elsif featured_media.media_type == âimageâ
increment image_index
endif
endcapture
assign media_index = media_index | plus: 1
-%}
-
{%- capture thumbnail_id -%}
Thumbnail-{{ section.id }}-0
{%- endcapture -%}
{{
featured_media.preview_image
| image_url: width: 416
| image_tag:
loading: 'lazy',
sizes: sizes,
widths: '54, 74, 104, 162, 208, 324, 416',
id: thumbnail_id,
alt: featured_media.alt
| escape
}}
{%- endif -%}
{%- for media in product.media -%}
{%- unless media.id == product.selected_or_first_available_variant.featured_media.id -%}
{%- liquid
capture media_index
if media.media_type == 'model'
increment model_index
elsif media.media_type == 'video' or media.media_type == 'external_video'
increment video_index
elsif media.media_type == 'image'
increment image_index
endif
endcapture
assign media_index = media_index | plus: 1
-%}
-
{%- if media.media_type == 'model' -%}
{%- render 'icon-3d-model' -%}
{%- elsif media.media_type == 'video' or media.media_type == 'external_video' -%}
{%- render 'icon-play' -%}
{%- endif -%}
{%- capture thumbnail_id -%}
Thumbnail-{{ section.id }}-{{ forloop.index }}
{%- endcapture -%}
{{
media.preview_image
| image_url: width: 416
| image_tag:
loading: 'lazy',
sizes: sizes,
widths: '54, 74, 104, 162, 208, 324, 416',
id: thumbnail_id,
alt: media.alt
| escape
}}
{%- endunless -%}
{%- endfor -%}
{% render 'icon-caret' %}
{%- endif -%}
///////// Third Step ///////////////
Open product-info.js file and replace all this code
if (!customElements.get(âproduct-infoâ)) {
customElements.define(
âproduct-infoâ,
class ProductInfo extends HTMLElement {
quantityInput = undefined;
quantityForm = undefined;
onVariantChangeUnsubscriber = undefined;
cartUpdateUnsubscriber = undefined;
abortController = undefined;
pendingRequestUrl = null;
preProcessHtmlCallbacks = ;
postProcessHtmlCallbacks = ;
constructor() {
super();
this.quantityInput = this.querySelector(â.quantity__inputâ);
}
connectedCallback() {
this.initializeProductSwapUtility();
this.onVariantChangeUnsubscriber = subscribe(
PUB_SUB_EVENTS.optionValueSelectionChange,
this.handleOptionValueChange.bind(this)
);
this.initQuantityHandlers();
this.dispatchEvent(new CustomEvent(âproduct-info:loadedâ, { bubbles: true }));
}
addPreProcessCallback(callback) {
this.preProcessHtmlCallbacks.push(callback);
}
initQuantityHandlers() {
if (!this.quantityInput) return;
this.quantityForm = this.querySelector(â.product-form__quantityâ);
if (!this.quantityForm) return;
this.setQuantityBoundries();
if (!this.dataset.originalSection) {
this.cartUpdateUnsubscriber = subscribe(PUB_SUB_EVENTS.cartUpdate, this.fetchQuantityRules.bind(this));
}
}
disconnectedCallback() {
this.onVariantChangeUnsubscriber();
this.cartUpdateUnsubscriber?.();
}
initializeProductSwapUtility() {
this.preProcessHtmlCallbacks.push((html) =>
html.querySelectorAll(â.scroll-triggerâ).forEach((element) => element.classList.add(âscroll-triggerâcancelâ))
);
this.postProcessHtmlCallbacks.push((newNode) => {
window?.Shopify?.PaymentButton?.init();
window?.ProductModel?.loadShopifyXR();
});
}
handleOptionValueChange({ data: { event, target, selectedOptionValues } }) {
if (!this.contains(event.target)) return;
this.resetProductFormState();
const productUrl = target.dataset.productUrl || this.pendingRequestUrl || this.dataset.url;
this.pendingRequestUrl = productUrl;
const shouldSwapProduct = this.dataset.url !== productUrl;
const shouldFetchFullPage = this.dataset.updateUrl === âtrueâ && shouldSwapProduct;
this.renderProductInfo({
requestUrl: this.buildRequestUrlWithParams(productUrl, selectedOptionValues, shouldFetchFullPage),
targetId: target.id,
callback: shouldSwapProduct
? this.handleSwapProduct(productUrl, shouldFetchFullPage)
: this.handleUpdateProductInfo(productUrl),
});
}
resetProductFormState() {
const productForm = this.productForm;
productForm?.toggleSubmitButton(true);
productForm?.handleErrorMessage();
}
handleSwapProduct(productUrl, updateFullPage) {
return (html) => {
this.productModal?.remove();
const selector = updateFullPage ? âproduct-info[id^=âMainProductâ]â : âproduct-infoâ;
const variant = this.getSelectedVariant(html.querySelector(selector));
this.updateURL(productUrl, variant?.id);
if (updateFullPage) {
document.querySelector(âhead titleâ).innerHTML = html.querySelector(âhead titleâ).innerHTML;
HTMLUpdateUtility.viewTransition(
document.querySelector(âmainâ),
html.querySelector(âmainâ),
this.preProcessHtmlCallbacks,
this.postProcessHtmlCallbacks
);
} else {
HTMLUpdateUtility.viewTransition(
this,
html.querySelector(âproduct-infoâ),
this.preProcessHtmlCallbacks,
this.postProcessHtmlCallbacks
);
}
};
}
renderProductInfo({ requestUrl, targetId, callback }) {
this.abortController?.abort();
this.abortController = new AbortController();
fetch(requestUrl, { signal: this.abortController.signal })
.then((response) => response.text())
.then((responseText) => {
this.pendingRequestUrl = null;
const html = new DOMParser().parseFromString(responseText, âtext/htmlâ);
callback(html);
})
.then(() => {
// set focus to last clicked option value
document.querySelector(#${targetId})?.focus();
})
.catch((error) => {
if (error.name === âAbortErrorâ) {
console.log(âFetch aborted by userâ);
} else {
console.error(error);
}
});
}
getSelectedVariant(productInfoNode) {
const selectedVariant = productInfoNode.querySelector(âvariant-selects [data-selected-variant]â)?.innerHTML;
return !!selectedVariant ? JSON.parse(selectedVariant) : null;
}
buildRequestUrlWithParams(url, optionValues, shouldFetchFullPage = false) {
const params = ;
!shouldFetchFullPage && params.push(section_id=${this.sectionId});
if (optionValues.length) {
params.push(option_values=${optionValues.join(',')});
}
return ${url}?${params.join('&')};
}
updateOptionValues(html) {
const variantSelects = html.querySelector(âvariant-selectsâ);
if (variantSelects) {
HTMLUpdateUtility.viewTransition(this.variantSelectors, variantSelects, this.preProcessHtmlCallbacks);
}
}
handleUpdateProductInfo(productUrl) {
return (html) => {
const variant = this.getSelectedVariant(html);
this.pickupAvailability?.update(variant);
this.updateOptionValues(html);
this.updateURL(productUrl, variant?.id);
this.updateVariantInputs(variant?.id);
if (!variant) {
this.setUnavailable();
return;
}
this.updateMedia(html, variant?.featured_media?.id,variant);
this.filterVariantImages(variant);
const updateSourceFromDestination = (id, shouldHide = (source) => false) => {
const source = html.getElementById(${id}-${this.sectionId});
const destination = this.querySelector(#${id}-${this.dataset.section});
if (source && destination) {
destination.innerHTML = source.innerHTML;
destination.classList.toggle(âhiddenâ, shouldHide(source));
}
};
updateSourceFromDestination(âpriceâ);
updateSourceFromDestination(âSkuâ, ({ classList }) => classList.contains(âhiddenâ));
updateSourceFromDestination(âInventoryâ, ({ innerText }) => innerText === ââ);
updateSourceFromDestination(âVolumeâ);
updateSourceFromDestination(âPrice-Per-Itemâ, ({ classList }) => classList.contains(âhiddenâ));
this.updateQuantityRules(this.sectionId, html);
this.querySelector(#Quantity-Rules-${this.dataset.section})?.classList.remove(âhiddenâ);
this.querySelector(#Volume-Note-${this.dataset.section})?.classList.remove(âhiddenâ);
this.productForm?.toggleSubmitButton(
html.getElementById(ProductSubmitButton-${this.sectionId})?.hasAttribute(âdisabledâ) ?? true,
window.variantStrings.soldOut
);
publish(PUB_SUB_EVENTS.variantChange, {
data: {
sectionId: this.sectionId,
html,
variant,
},
});
};
}
filterVariantImages(varObj){
console.log(âinfoâ)
if(varObj.featured_image && varObj.featured_image.alt){
document.querySelectorAll(â[thumbnail-alt]â).forEach(img => img.style.display = ânoneâ);
const currentImageAlt = varObj.featured_image.alt;
const thumbnailSelector= [thumbnail-alt= '${currentImageAlt}'];
document.querySelectorAll(thumbnailSelector).forEach( (img) => {
img.style.display = âblockâ;
})
} else{
document.querySelectorAll(â[thumbnail-alt]â).forEach(img => img.style.display = âblockâ);
}
}
updateVariantInputs(variantId) {
this.querySelectorAll(
#product-form-${this.dataset.section}, #product-form-installment-${this.dataset.section}
).forEach((productForm) => {
const input = productForm.querySelector(âinput[name=âidâ]â);
input.value = variantId ?? ââ;
input.dispatchEvent(new Event(âchangeâ, { bubbles: true }));
});
}
updateURL(url, variantId) {
this.querySelector(âshare-buttonâ)?.updateUrl(
${window.shopUrl}${url}${variantId ? ?variant=${variantId} : ''}
);
if (this.dataset.updateUrl === âfalseâ) return;
window.history.replaceState({}, ââ, ${url}${variantId ? ?variant=${variantId} : ''});
}
setUnavailable() {
this.productForm?.toggleSubmitButton(true, window.variantStrings.unavailable);
const selectors = [âpriceâ, âInventoryâ, âSkuâ, âPrice-Per-Itemâ, âVolume-Noteâ, âVolumeâ, âQuantity-Rulesâ]
.map((id) => #${id}-${this.dataset.section})
.join(', ');
document.querySelectorAll(selectors).forEach(({ classList }) => classList.add(âhiddenâ));
}
updateMedia(html, variantFeaturedMediaId,varObj) {
if (!variantFeaturedMediaId) return;
const mediaGallerySource = this.querySelector(âmedia-gallery ulâ);
const mediaGalleryDestination = html.querySelector(media-gallery ul);
const refreshSourceData = () => {
if (this.hasAttribute(âdata-zoom-on-hoverâ)) enableZoomOnHover(2);
const mediaGallerySourceItems = Array.from(mediaGallerySource.querySelectorAll(âli[data-media-id]â));
const sourceSet = new Set(mediaGallerySourceItems.map((item) => item.dataset.mediaId));
const sourceMap = new Map(
mediaGallerySourceItems.map((item, index) => [item.dataset.mediaId, { item, index }])
);
return [mediaGallerySourceItems, sourceSet, sourceMap];
};
if (mediaGallerySource && mediaGalleryDestination) {
let [mediaGallerySourceItems, sourceSet, sourceMap] = refreshSourceData();
const mediaGalleryDestinationItems = Array.from(
mediaGalleryDestination.querySelectorAll(âli[data-media-id]â)
);
const destinationSet = new Set(mediaGalleryDestinationItems.map(({ dataset }) => dataset.mediaId));
let shouldRefresh = false;
// add items from new data not present in DOM
for (let i = mediaGalleryDestinationItems.length - 1; i >= 0; iâ) {
if (!sourceSet.has(mediaGalleryDestinationItems[i].dataset.mediaId)) {
mediaGallerySource.prepend(mediaGalleryDestinationItems[i]);
shouldRefresh = true;
}
}
// remove items from DOM not present in new data
for (let i = 0; i < mediaGallerySourceItems.length; i++) {
if (!destinationSet.has(mediaGallerySourceItems[i].dataset.mediaId)) {
mediaGallerySourceItems[i].remove();
shouldRefresh = true;
}
}
// refresh
if (shouldRefresh) [mediaGallerySourceItems, sourceSet, sourceMap] = refreshSourceData();
// if media galleries donât match, sort to match new data order
mediaGalleryDestinationItems.forEach((destinationItem, destinationIndex) => {
const sourceData = sourceMap.get(destinationItem.dataset.mediaId);
if (sourceData && sourceData.index !== destinationIndex) {
mediaGallerySource.insertBefore(
sourceData.item,
mediaGallerySource.querySelector(li:nth-of-type(${destinationIndex + 1}))
);
// refresh source now that it has been modified
[mediaGallerySourceItems, sourceSet, sourceMap] = refreshSourceData();
}
});
}
// set featured media as active in the media gallery
this.querySelector(media-gallery)?.setActiveMedia?.(
${this.dataset.section}-${variantFeaturedMediaId},
true,
varObj.featured_image.alt
);
// update media modal
const modalContent = this.productModal?.querySelector(.product-media-modal__content);
const newModalContent = html.querySelector(product-modal .product-media-modal__content);
if (modalContent && newModalContent) modalContent.innerHTML = newModalContent.innerHTML;
}
setQuantityBoundries() {
const data = {
cartQuantity: this.quantityInput.dataset.cartQuantity ? parseInt(this.quantityInput.dataset.cartQuantity) : 0,
min: this.quantityInput.dataset.min ? parseInt(this.quantityInput.dataset.min) : 1,
max: this.quantityInput.dataset.max ? parseInt(this.quantityInput.dataset.max) : null,
step: this.quantityInput.step ? parseInt(this.quantityInput.step) : 1,
};
let min = data.min;
const max = data.max === null ? data.max : data.max - data.cartQuantity;
if (max !== null) min = Math.min(min, max);
if (data.cartQuantity >= data.min) min = Math.min(min, data.step);
this.quantityInput.min = min;
if (max) {
this.quantityInput.max = max;
} else {
this.quantityInput.removeAttribute(âmaxâ);
}
this.quantityInput.value = min;
publish(PUB_SUB_EVENTS.quantityUpdate, undefined);
}
fetchQuantityRules() {
const currentVariantId = this.productForm?.variantIdInput?.value;
if (!currentVariantId) return;
this.querySelector(â.quantity__rules-cart .loading__spinnerâ).classList.remove(âhiddenâ);
fetch(${this.dataset.url}?variant=${currentVariantId}§ion_id=${this.dataset.section})
.then((response) => response.text())
.then((responseText) => {
const html = new DOMParser().parseFromString(responseText, âtext/htmlâ);
this.updateQuantityRules(this.dataset.section, html);
})
.catch((e) => console.error(e))
.finally(() => this.querySelector(â.quantity__rules-cart .loading__spinnerâ).classList.add(âhiddenâ));
}
updateQuantityRules(sectionId, html) {
if (!this.quantityInput) return;
this.setQuantityBoundries();
const quantityFormUpdated = html.getElementById(Quantity-Form-${sectionId});
const selectors = [â.quantity__inputâ, â.quantity__rulesâ, â.quantity__labelâ];
for (let selector of selectors) {
const current = this.quantityForm.querySelector(selector);
const updated = quantityFormUpdated.querySelector(selector);
if (!current || !updated) continue;
if (selector === â.quantity__inputâ) {
const attributes = [âdata-cart-quantityâ, âdata-minâ, âdata-maxâ, âstepâ];
for (let attribute of attributes) {
const valueUpdated = updated.getAttribute(attribute);
if (valueUpdated !== null) {
current.setAttribute(attribute, valueUpdated);
} else {
current.removeAttribute(attribute);
}
}
} else {
current.innerHTML = updated.innerHTML;
}
}
}
get productForm() {
return this.querySelector(product-form);
}
get productModal() {
return document.querySelector(#ProductModal-${this.dataset.section});
}
get pickupAvailability() {
return this.querySelector(pickup-availability);
}
get variantSelectors() {
return this.querySelector(âvariant-selectsâ);
}
get relatedProducts() {
const relatedProductsSectionId = SectionId.getIdForSection(
SectionId.parseId(this.sectionId),
ârelated-productsâ
);
return document.querySelector(product-recommendations[data-section-id^="${relatedProductsSectionId}"]);
}
get quickOrderList() {
const quickOrderListSectionId = SectionId.getIdForSection(
SectionId.parseId(this.sectionId),
âquick_order_listâ
);
return document.querySelector(quick-order-list[data-id^="${quickOrderListSectionId}"]);
}
get sectionId() {
return this.dataset.originalSection || this.dataset.section;
}
}
);
}
Let me know if this helps your cause or you need further customizations.