Multiple variant images in Shopify

In this tutorial we’ll explore how to display Images Specific to the Selected Variant in Dawn, Refresh, Craft, Spotlight, Sense.

Please follow video guide

Snippet: product-media-gallery

Liquid
{% 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
  assign has_variant = false
  if product.has_only_default_variant == false
    assign has_variant = true
  endif

  if has_variant
    assign media_count = product.selected_or_first_available_variant.metafields.custom.variant_gallery.value.count
  else
    assign media_count = product.media.size
  endif

  if media_count == 0 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 }}"
  has_variant="{{ has_variant }}"
>
  <div id="GalleryStatus-{{ section.id }}" class="visually-hidden" role="status"></div>
  <slider-component id="GalleryViewer-{{ section.id }}" class="slider-mobile-gutter">
    <a class="skip-to-content-link button visually-hidden quick-add-hidden" href="#ProductInfo-{{ section.id }}">
      {{ 'accessibility.skip_to_product_info' | t }}
    </a>
    <ul
      id="Slider-Gallery-{{ section.id }}"
      class="product__media-list contains-media grid grid--peek list-unstyled slider slider--mobile"
      role="list"
    >
      {% unless has_variant %}
        {%- 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 -%}
            <li
              id="Slide-{{ section.id }}-{{ media.id }}"
              class="product__media-item grid__item slider__slide{% if product.selected_or_first_available_variant.featured_media == nil and forloop.index == 1 %} is-active{% endif %}{% if media.media_type != 'image' %} product__media-item--full{% endif %}{% if settings.animations_reveal_on_scroll %} scroll-trigger animate--fade-in{% endif %}"
              data-media-id="{{ section.id }}-{{ 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
              %}
            </li>
          {%- endunless -%}
        {%- endfor -%}
      {% endunless %}
      {%- if product.selected_or_first_available_variant.featured_media != null -%}
        {% style %}.product__modal-opener{pointer-events: none;}.product__media-icon--lightbox{display: none;}{% endstyle %}
        {%- assign featured_media = product.selected_or_first_available_variant.featured_media -%}
        <li
          id="Slide-{{ section.id }}-{{ featured_media.id }}"
          class="product__media-item grid__item slider__slide is-active"
          data-media-id="{{ section.id }}-{{ featured_media.id }}"
        >
          {%- 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,
            media_width: media_width,
            media_fit: section.settings.media_fit,
            constrain_to_viewport: section.settings.constrain_to_viewport,
            lazy_load: false
          %}
        </li>
        {% comment %} variant gallery {% endcomment %}
        {% unless limit %}
        {% if product.selected_or_first_available_variant.metafields.custom.variant_gallery.value %}
          {%- capture sizes -%}
              (min-width: {{ settings.page_width }}px) {{ settings.page_width | minus: 100 | times: media_width | divided_by: desktop_columns | round }}px, (min-width: 990px) calc({{ media_width | times: 100 | divided_by: desktop_columns }}vw - 10rem), (min-width: 750px) calc((100vw - 11.5rem) / 2), calc(100vw / {{ mobile_columns }} - 4rem)
            {%- endcapture -%}
          {% for gallery in product.selected_or_first_available_variant.metafields.custom.variant_gallery.value %}
            {% assign img_ratio = gallery.image.aspect_ratio %}
            <li
              id="Slide-slide-{{ section.id }}-{{ forloop.index }}"
              class="product__media-item grid__item slider__slide{% if product.selected_or_first_available_variant.featured_media == nil and forloop.index == 1 %} is-active{% endif %}"
              data-media-id="{{ section.id }}-{{ forloop.index }}"
            >
              <div
                style="--ratio: {{ img_ratio }}; --preview-ratio: {{ img_ratio }};"
                class="product-media-container media-type-image media-fit-cover global-media-settings gradient constrain-height"
              >
                <div class="product__media media media--transparent">
                  {{
                    gallery
                    | image_url: width: 1946
                    | image_tag:
                      loading: 'lazy',
                      fetchpriority: 'low',
                      widths: '400, 620, 690, 810, 980, 1240, 1380, 1620, 1960',
                      sizes: sizes
                  }}
                </div>
              </div>
            </li>
          {% endfor %}
        {% endif %}
        {% endunless %}
      {%- endif -%}
    </ul>
    <div class="slider-buttons quick-add-hidden{% if hide_mobile_slider %} small-hide{% endif %}">
      <button
        type="button"
        class="slider-button slider-button--prev"
        name="previous"
        aria-label="{{ 'general.slider.previous_slide' | t }}"
      >
        <span class="svg-wrapper">
          {{- 'icon-caret.svg' | inline_asset_content -}}
        </span>
      </button>
      <div class="slider-counter caption">
        <span class="slider-counter--current">1</span>
        <span aria-hidden="true"> / </span>
        <span class="visually-hidden">{{ 'general.slider.of' | t }}</span>
        <span class="slider-counter--total">{{ media_count }}</span>
      </div>
      <button
        type="button"
        class="slider-button slider-button--next"
        name="next"
        aria-label="{{ 'general.slider.next_slide' | t }}"
      >
        <span class="svg-wrapper">
          {{- 'icon-caret.svg' | inline_asset_content -}}
        </span>
      </button>
    </div>
  </slider-component>
  {%- if media_count >= 1
    and section.settings.gallery_layout contains 'thumbnail'
    or section.settings.mobile_thumbnails == 'show'
  -%}
    <slider-component
      id="GalleryThumbnails-{{ section.id }}"
      class="thumbnail-slider slider-mobile-gutter quick-add-hidden{% unless section.settings.gallery_layout contains 'thumbnail' %} medium-hide large-up-hide{% endunless %}{% if section.settings.mobile_thumbnails != 'show' %} small-hide{% endif %}{% if media_count <= 3 %} thumbnail-slider--no-slide{% endif %}"
    >
      <button
        type="button"
        class="slider-button slider-button--prev{% if media_count <= 3 %} small-hide{% endif %}{% if media_count <= 4 %} medium-hide large-up-hide{% endif %}"
        name="previous"
        aria-label="{{ 'general.slider.previous_slide' | t }}"
        aria-controls="GalleryThumbnails-{{ section.id }}"
        data-step="3"
      >
        <span class="svg-wrapper">
          {{- 'icon-caret.svg' | inline_asset_content -}}
        </span>
      </button>
      <ul
        id="Slider-Thumbnails-{{ section.id }}"
        class="thumbnail-list list-unstyled slider slider--mobile{% if section.settings.gallery_layout == 'thumbnail_slider' %} slider--tablet-up{% endif %}"
      >
        {%- 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
          -%}
          <li
            id="Slide-Thumbnails-{{ section.id }}-0"
            class="thumbnail-list__item slider__slide{% if section.settings.hide_variants and variant_images contains featured_media.src %} thumbnail-list_item--variant{% endif %}"
            data-target="{{ section.id }}-{{ featured_media.id }}"
            data-media-position="{{ media_index }}"
          >
            {%- capture thumbnail_id -%}
                Thumbnail-{{ section.id }}-0
              {%- endcapture -%}
            <button
              class="thumbnail global-media-settings global-media-settings--no-shadow"
              aria-label="{%- if featured_media.media_type == 'image' -%}{{ 'products.product.media.load_image' | t: index: media_index }}{%- elsif featured_media.media_type == 'model' -%}{{ 'products.product.media.load_model' | t: index: media_index }}{%- elsif featured_media.media_type == 'video' or featured_media.media_type == 'external_video' -%}{{ 'products.product.media.load_video' | t: index: media_index }}{%- endif -%}"
              aria-current="true"
              aria-controls="GalleryViewer-{{ section.id }}"
              aria-describedby="{{ thumbnail_id }}"
            >
              {{
                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
              }}
            </button>
          </li>
        {%- endif -%}
        {% unless has_variant %}
          {%- 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
              -%}
              <li
                id="Slide-Thumbnails-{{ section.id }}-{{ forloop.index }}"
                class="thumbnail-list__item slider__slide{% if section.settings.hide_variants and variant_images contains media.src %} thumbnail-list_item--variant{% endif %}"
                data-target="{{ section.id }}-{{ media.id }}"
                data-media-position="{{ media_index }}"
              >
                {%- if media.media_type == 'model' -%}
                  <span class="thumbnail__badge" aria-hidden="true">
                    <span class="svg-wrapper">
                      {{- 'icon-3d-model.svg' | inline_asset_content -}}
                    </span>
                  </span>
                {%- elsif media.media_type == 'video' or media.media_type == 'external_video' -%}
                  <span class="thumbnail__badge" aria-hidden="true">
                    <span class="svg-wrapper">
                      {{- 'icon-play.svg' | inline_asset_content -}}
                    </span>
                  </span>
                {%- endif -%}
                {%- capture thumbnail_id -%}
                  Thumbnail-{{ section.id }}-{{ forloop.index }}
                {%- endcapture -%}
                <button
                  class="thumbnail global-media-settings global-media-settings--no-shadow"
                  aria-label="{%- if media.media_type == 'image' -%}{{ 'products.product.media.load_image' | t: index: media_index }}{%- elsif media.media_type == 'model' -%}{{ 'products.product.media.load_model' | t: index: media_index }}{%- elsif media.media_type == 'video' or media.media_type == 'external_video' -%}{{ 'products.product.media.load_video' | t: index: media_index }}{%- endif -%}"
                  {% if media == product.selected_or_first_available_variant.featured_media
                    or product.selected_or_first_available_variant.featured_media == null
                    and forloop.index == 1
                  %}
                    aria-current="true"
                  {% endif %}
                  aria-controls="GalleryViewer-{{ section.id }}"
                  aria-describedby="{{ thumbnail_id }}"
                >
                  {{
                    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
                  }}
                </button>
              </li>
            {%- endunless -%}
          {%- endfor -%}
        {% endunless %}
        {% comment %} variant gallery {% endcomment %}
        {% for gallery in product.selected_or_first_available_variant.metafields.custom.variant_gallery.value %}
          <li
            id="Slide-Thumbnails-thumbnail-{{ section.id }}-{{ forloop.index }}"
            class="thumbnail-list__item slider__slide{% if section.settings.hide_variants and variant_images contains media.src %} thumbnail-list_item--variant{% endif %}"
            data-target="{{ section.id }}-{{ forloop.index }}"
            data-media-position="{{ forloop.index | plus: 1 }}"
          >
            <button class="thumbnail global-media-settings global-media-settings--no-shadow">
              {{
                gallery
                | image_url: width: 416
                | image_tag: loading: 'lazy', sizes: sizes, widths: '54, 74, 104, 162, 208, 324, 416', id: thumbnail_id
              }}
            </button>
          </li>
        {%- endfor -%}
      </ul>
      <button
        type="button"
        class="slider-button slider-button--next{% if media_count <= 3 %} small-hide{% endif %}{% if media_count <= 4 %} medium-hide large-up-hide{% endif %}"
        name="next"
        aria-label="{{ 'general.slider.next_slide' | t }}"
        aria-controls="GalleryThumbnails-{{ section.id }}"
        data-step="3"
      >
        <span class="svg-wrapper">
          {{- 'icon-caret.svg' | inline_asset_content -}}
        </span>
      </button>
    </slider-component>
  {%- endif -%}
</media-gallery>

JavaScript code snippet – 15 or later ( product-info.js )

JavaScript
if (this.querySelector('media-gallery').getAttribute('has_variant') === 'true') {
  const mediaGallerySource = this.querySelector('media-gallery');
  const mediaGalleryDestination = html.querySelector(`media-gallery`);
  mediaGallerySource.outerHTML = mediaGalleryDestination.outerHTML;
  return;
}

JavaScript code snippet – 14 or earlier ( global.js )

JavaScript
if (document.querySelector(`[id^="MediaGallery-${this.dataset.section}"]`).getAttribute('has_variant') === 'true') 
{
  const mediaGallerySource = document.querySelector(`[id^="MediaGallery-${this.dataset.section}"]`);
  const mediaGalleryDestination = html.querySelector(`[id^="MediaGallery-${this.dataset.section}"]`);
  mediaGallerySource.outerHTML = mediaGalleryDestination.outerHTML;
  return;
}