How do I assign colour variant to product image for that specific colour on dawn theme 15.0 - code

@SMH2020 I did the part where you show images of only the selected variant color, but dawn 15.0 the code base has changed massively.

I will need some time to figure out the onVariant change functionality, it does work now but I have to refresh the page to see results. I will take a look into it.

Thanks

@SMH2020 this took a lot of looking up the code but finally was able to implement this alhamdulillah.

Hello there, I’m new to Shopify! I’m managing to get some of the layout and navigation to work as intended but I was wondering if this is the thread to inquire about the following problem:

I would like the customer to click on a colour variant of a product and for the main image and the thumbnail selection to display just the images/products of that colour. Is this possible in Dawn 15? I have set up the colour swatches but when I choose a colour variant of the product, the main image changes but the rest of the images in the thumbnails remain the same. So the customer will need to go through all of the images to find other images/products of that colour.

I know this can be done in previous versions of the Dawn theme but when code customisation is required, it doesn’t seem to match in the latest V15 of Dawn.

Does anyone know how to do this? Many thanks in advance for any help!

Thank you!

@Vegan_Stallion yes I have been able to implement this on other layouts like stacked or 2 columns, not on thumbnails yet. But it won’t be tough in shaa Allah as I believe I know just how to do it.

Will update you when done

Thanks

Thank you! I’m looking for apps in Shopify that might be able to help but I’m not sure if they work with V15 of Dawn theme. I can’t find one that allows me to do what I want yet. Thanks for your reply!

@Vegan_Stallion No worries. i hope you dont need an app with this solution. 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

Shadab_dev_2-1720057077113.png

hope you get the point.

////////// Second step ////////

Open your product-media-gallery.liquid file in your code editor and add 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 }}
    {%- if product.selected_or_first_available_variant.featured_media != null -%} {%- assign featured_media = product.selected_or_first_available_variant.featured_media -%}
  • {%- assign media_position = 1 -%} {% render 'product-thumbnail', media: featured_media, media_count: media_count, position: media_position, desktop_layout: section.settings.gallery_layout, mobile_layout: section.settings.mobile_thumbnails, loop: section.settings.enable_video_looping, modal_id: section.id, xr_button: true, media_width: media_width, media_fit: section.settings.media_fit, constrain_to_viewport: section.settings.constrain_to_viewport, lazy_load: false %}
  • {%- endif -%} {%- for media in product.media -%} {% if media_position >= limit or media_position >= 1 and section.settings.hide_variants and variant_images contains media.src %} {% continue %} {% endif %}

    {%- unless media.id == product.selected_or_first_available_variant.featured_media.id -%}

  • {%- liquid assign media_position = media_position | default: 0 | plus: 1 assign lazy_load = false if media_position > 1 assign lazy_load = true endif -%} {% render 'product-thumbnail', media: media, media_count: media_count, position: media_position, desktop_layout: section.settings.gallery_layout, mobile_layout: section.settings.mobile_thumbnails, loop: section.settings.enable_video_looping, modal_id: section.id, xr_button: true, media_width: media_width, media_fit: section.settings.media_fit, constrain_to_viewport: section.settings.constrain_to_viewport, lazy_load: lazy_load %}
  • {%- endunless -%} {%- endfor -%}
{% 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 add this.

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);
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){
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) {
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
);

// 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}&section_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;
}
}
);
}

This should probably do what you are looking for, i hope it does.

If this helps your case please do like and accept it as a solution. Also if you need better customization or any related queries feel free to reach out at

Email ME

Buy Me A Coffee, If you feel i was helpful and deserve it. Will be a motivating factor.

Thanks anyways.

Hi,

Just wondered if anyone can help me out for trade theme 15.0 also.

Kind regards,

KPS Clothing

@kpsclothing do you want to show all the products or do you want to show only the product images whose color matches with the selected color variant?

Is trade theme a premium theme?

we want only the product images whose color matches the selected color variant.

trade theme is a free theme.

@kpsclothing I managed to accomplish that in the latest dawn theme as you can see below.

Let me check if I have the trade theme available if it is I can code it on my development store or else I will need collaborator access to your store.

Thanks

@kpsclothing I have it. This was kind of easy since the code base for the trade theme is same as of the latest version of dawn theme. Please tell me me the layout you are using to show your products.

The code i have now works for stacked and 2 columns. if you are using thumbnails let me know i will update the code. I tried to implement both but not achieved that yet. So let me know which layout you will be using i will send that code accordingly

Thanks

Yes, we are using thumbnails and thumbnail carousels also. Can we have a “show color option as variant image” for this theme?

@kpsclothing Sorry about this i should have cleared this before. I was talking about the layout of your product images not your variant swatches. So the images of the product you uploaded through product media. I hope you get the point

Thanks

Hi Shadab,

That worked perfectly! However, I didn’t realise I had to replace the code with yours. I thought I had to add it on at the bottom but then I tried replacing all of it with your code, then it worked!

Thank you so much!

I bought you a coffee too with your link!

Thank you again for your help!

John


yes, we are talking about both the product layout uploaded through product media and the variant swatches.

@kpsclothing I dont think there’s any change needed to the swatches. The number of colors you have in your color variant only that much swatches will show up, it does not get duplicated.

Challenge is to not show all the products on selection of a particular variant color and I am going to update that to you pretty soon in shaa Allah

Thanks

Hi @Vegan_Stallion all is well that ends well, ri8? Glad it worked for you and it was what you were looking for.

Thank you for that tip means a lot

Let me know if you need any further help regarding code, theme customization, a bit of design stuff. You have my email.

Thank you again for that tip

Feels good to be useful to someone :sweat_smile:

Thanks

@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

Shadab_dev_2-1720275457286.png

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 }}
    {%- if product.selected_or_first_available_variant.featured_media != null -%} {%- assign featured_media = product.selected_or_first_available_variant.featured_media -%}
  • {%- assign media_position = 1 -%} {% render 'product-thumbnail', media: featured_media, media_count: media_count, position: media_position, desktop_layout: section.settings.gallery_layout, mobile_layout: section.settings.mobile_thumbnails, loop: section.settings.enable_video_looping, modal_id: section.id, xr_button: true, media_width: media_width, media_fit: section.settings.media_fit, constrain_to_viewport: section.settings.constrain_to_viewport, lazy_load: false %}
  • {%- endif -%} {%- for media in product.media -%} {% if media_position >= limit or media_position >= 1 and section.settings.hide_variants and variant_images contains media.src %} {% continue %} {% endif %}

    {%- unless media.id == product.selected_or_first_available_variant.featured_media.id -%}

  • {%- liquid assign media_position = media_position | default: 0 | plus: 1 assign lazy_load = false if media_position > 1 assign lazy_load = true endif -%} {% render 'product-thumbnail', media: media, media_count: media_count, position: media_position, desktop_layout: section.settings.gallery_layout, mobile_layout: section.settings.mobile_thumbnails, loop: section.settings.enable_video_looping, modal_id: section.id, xr_button: true, media_width: media_width, media_fit: section.settings.media_fit, constrain_to_viewport: section.settings.constrain_to_viewport, lazy_load: lazy_load %}
  • {%- endunless -%} {%- endfor -%}
{% 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}&section_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.

Thank You, the code is working!

Also, can we have the color option as variant image in variant picker?

@kpsclothing great it worked.

Yeah that is achievable. Will update you. Please do mark the previous one as a solution

Thanks