Blog

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.

ByUlrich Lehner Published TagsShopify · E-commerce · JavaScript · UX

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.