Only show selected variant images in Shopify for better UX
A small Liquid + JavaScript pattern for Shopify's Debut theme that makes product galleries show only the images for the variant a customer is actively looking at.
Shopify's default product-page behavior is to show every image associated with a product, no matter which variant the customer has selected. On a product with several colors or material options, that means the gallery becomes a long scroll of near-duplicates, and the thing the customer is actually trying to evaluate — this variant — gets diluted.
There's a simple way to fix it with a few lines of Liquid and JavaScript. The snippets below are written against Shopify's Debut theme and its slate.Variants module, but the pattern ports cleanly to any theme that exposes a variant-change event.
Tag each thumbnail with its variant IDs
Edit templates/product.dynamic_thumbnails.liquid. Inside the {% for media in product.media %} loop, collect the variants that own each media item and emit the ID list as a data-variant-ids attribute on the thumbnail element:
{%- assign variant_images = product.images | where: 'attached_to_variant?', true | map: 'src' -%}
{% for media in product.media %}
<li class="product-single__thumbnails-item product-single__thumbnails-item--{{ section.settings.media_size }}{% if enable_thumbnail_slides %} product-single__thumbnails-item-slide{% endif %} js"
{% if enable_thumbnail_slides %}data-slider-slide-index="{{ forloop.index0 }}" data-slider-item{% endif %}
{%- if variant_images contains media.src -%}
{%- assign variants = '' | split: '' -%}
{%- assign tmp_variants = product.images | where: 'src', media.src | map: 'variants' -%}
{%- for variant in tmp_variants -%}
{%- assign variants = variants | concat: variant -%}
{%- endfor -%}
data-variant-ids="{{ variants | map: 'id' | uniq | join: ',' }}"
{%- endif -%}>
<a href="{{ media.preview_image | img_url: product_image_zoom_size, scale: product_image_scale }}"
class="text-link product-single__thumbnail product-single__thumbnail--{{ section.id }}"
data-thumbnail-id="{{ section.id }}-{{ media.id }}">
<!-- …existing thumbnail markup… -->
</a>
</li>
{% endfor %}
Thumbnails tied to specific variants now carry the variant IDs that own them; shared/marketing images stay un-tagged and will remain visible in every state.
Add an _updateThumbnails helper
In assets/theme.js, add a new method on the Variants prototype that hides every tagged thumbnail whose list doesn't include the active variant. Un-tagged thumbnails are left alone:
/**
* Update thumbnails visibility
*
* @param {object} variant - Currently selected variant
*/
_updateThumbnails: function(variant) {
const allThumbs = document.querySelectorAll('[data-variant-ids]');
const keepThumbs = [];
allThumbs.forEach(function(el) {
const variantIds = el.dataset.variantIds.split(',');
if (!variantIds.length) {
return;
}
if (variantIds.includes('' + variant.id)) {
keepThumbs.push(el);
}
});
if (!keepThumbs.length) {
return;
}
allThumbs.forEach(function(el) {
el.classList.add('hide');
});
keepThumbs.forEach(function(el) {
el.classList.remove('hide');
});
},
Wire it up in two spots
Two places inside assets/theme.js need to call the new helper: the variant-change handler (so the gallery reacts when the shopper picks a new option) and the Variants constructor (so the initial render is correct for whatever variant the page loads with, including ?variant= deep-links).
1. In the _onSelectChange handler, alongside the existing _updateImages / _updatePrice calls:
/**
* Event handler for when a variant input changes.
*/
_onSelectChange: function() {
var variant = this._getVariantFromOptions();
this.container.dispatchEvent(
new CustomEvent('variantChange', {
detail: { variant: variant },
bubbles: true,
cancelable: true
})
);
if (!variant) {
return;
}
this._updateMasterSelect(variant);
this._updateImages(variant);
this._updateThumbnails(variant);
this._updatePrice(variant);
this._updateSKU(variant);
this.currentVariant = variant;
if (this.enableHistoryState) {
this._updateHistoryState(variant);
}
},
2. At the bottom of the Variants constructor, after the change listeners are bound:
/**
* Variant constructor
*
* @param {object} options - Settings from `product.js`
*/
function Variants(options) {
this.container = options.container;
this.product = options.product;
this.originalSelectorId = options.originalSelectorId;
this.enableHistoryState = options.enableHistoryState;
this.singleOptions = this.container.querySelectorAll(
options.singleOptionSelector
);
this.currentVariant = this._getVariantFromOptions();
this.singleOptions.forEach(
function(option) {
option.addEventListener('change', this._onSelectChange.bind(this));
}.bind(this)
);
// Initialize thumbnails
this._updateThumbnails(this.currentVariant);
}
That's it
Small, no framework, no plugin. If a shop later adopts Shopify's own variant-linked image feature, you swap this out for that. Until then, this pattern gets you 90% of the experience at 2% of the code.