Specifications:
- Show the featured image of each variant of a product on the collection page without duplicated images (we generally want to showcase each color variant)
- When we click on a product it automatically selects the same variant that we clicked on
- Show the value of the option next to the title and same the price of the variant if different than product price
Instructions to install the code snippet
Step – 1: Setup your products
- We have a product with multiple variants for the option we want to show
- Whether Color is the first or second option doesn’t matter
- Each variant should have a featured image
Step – 2: Create the file card-variant.liquid
Start by duplicating your theme
and click on edit code on your fresh copy.
After that, click on add a new snippet and name it card-variant
Now copy the code below and paste the code inside the created file
{{ 'component-rating.css' | asset_url | stylesheet_tag }}
{{ 'component-volume-pricing.css' | asset_url | stylesheet_tag }}
{%- if card_variant and card_variant != empty -%}
{%- liquid
assign ratio = 1
if card_variant.featured_media and media_aspect_ratio == 'portrait'
assign ratio = 0.8
elsif card_variant.featured_media and media_aspect_ratio == 'adapt'
assign ratio = card_variant.featured_media.aspect_ratio
endif
if ratio == 0 or ratio == null
assign ratio = 1
endif
-%}
<div class="card-wrapper product-card-wrapper underline-links-hover">
<div
class="
card card--{{ settings.card_style }}
{% if card_variant.featured_media %} card--media{% else %} card--text{% endif %}
{% if settings.card_style == 'card' %} color-{{ settings.card_color_scheme }} gradient{% endif %}
{% if image_shape and image_shape != 'default' %} card--shape{% endif %}
{% if extend_height %} card--extend-height{% endif %}
{% if card_variant.featured_media == nil and settings.card_style == 'card' %} ratio{% endif %}
{% if horizontal_class %} card--horizontal{% endif %}
"
style="--ratio-percent: {{ 1 | divided_by: ratio | times: 100 }}%;"
>
<div
class="card__inner {% if settings.card_style == 'standard' %}color-{{ settings.card_color_scheme }} gradient{% endif %}{% if card_variant.featured_media or settings.card_style == 'standard' %} ratio{% endif %}"
style="--ratio-percent: {{ 1 | divided_by: ratio | times: 100 }}%;"
>
{%- if card_variant.featured_media -%}
<div class="card__media{% if image_shape and image_shape != 'default' %} shape--{{ image_shape }} color-{{ settings.card_color_scheme }} gradient{% endif %}">
<div class="media media--transparent media--hover-effect">
{% comment %}theme-check-disable ImgLazyLoading{% endcomment %}
<img
srcset="
{%- if card_variant.featured_media.width >= 165 -%}{{ card_variant.featured_media | image_url: width: 165 }} 165w,{%- endif -%}
{%- if card_variant.featured_media.width >= 360 -%}{{ card_variant.featured_media | image_url: width: 360 }} 360w,{%- endif -%}
{%- if card_variant.featured_media.width >= 533 -%}{{ card_variant.featured_media | image_url: width: 533 }} 533w,{%- endif -%}
{%- if card_variant.featured_media.width >= 720 -%}{{ card_variant.featured_media | image_url: width: 720 }} 720w,{%- endif -%}
{%- if card_variant.featured_media.width >= 940 -%}{{ card_variant.featured_media | image_url: width: 940 }} 940w,{%- endif -%}
{%- if card_variant.featured_media.width >= 1066 -%}{{ card_variant.featured_media | image_url: width: 1066 }} 1066w,{%- endif -%}
{{ card_variant.featured_media | image_url }} {{ card_variant.featured_media.width }}w
"
src="{{ card_variant.featured_media | image_url: width: 533 }}"
sizes="(min-width: {{ settings.page_width }}px) {{ settings.page_width | minus: 130 | divided_by: 4 }}px, (min-width: 990px) calc((100vw - 130px) / 4), (min-width: 750px) calc((100vw - 120px) / 3), calc((100vw - 35px) / 2)"
alt="{{ card_variant.featured_media.alt | escape }}"
class="motion-reduce"
{% unless lazy_load == false %}
loading="lazy"
{% endunless %}
width="{{ card_variant.featured_media.width }}"
height="{{ card_variant.featured_media.height }}"
>
{% comment %}theme-check-enable ImgLazyLoading{% endcomment %}
{%- if card_variant.media[1] != null and show_secondary_image -%}
<img
srcset="
{%- if card_variant.media[1].width >= 165 -%}{{ card_variant.media[1] | image_url: width: 165 }} 165w,{%- endif -%}
{%- if card_variant.media[1].width >= 360 -%}{{ card_variant.media[1] | image_url: width: 360 }} 360w,{%- endif -%}
{%- if card_variant.media[1].width >= 533 -%}{{ card_variant.media[1] | image_url: width: 533 }} 533w,{%- endif -%}
{%- if card_variant.media[1].width >= 720 -%}{{ card_variant.media[1] | image_url: width: 720 }} 720w,{%- endif -%}
{%- if card_variant.media[1].width >= 940 -%}{{ card_variant.media[1] | image_url: width: 940 }} 940w,{%- endif -%}
{%- if card_variant.media[1].width >= 1066 -%}{{ card_variant.media[1] | image_url: width: 1066 }} 1066w,{%- endif -%}
{{ card_variant.media[1] | image_url }} {{ card_variant.media[1].width }}w
"
src="{{ card_variant.media[1] | image_url: width: 533 }}"
sizes="(min-width: {{ settings.page_width }}px) {{ settings.page_width | minus: 130 | divided_by: 4 }}px, (min-width: 990px) calc((100vw - 130px) / 4), (min-width: 750px) calc((100vw - 120px) / 3), calc((100vw - 35px) / 2)"
alt=""
class="motion-reduce"
loading="lazy"
width="{{ card_variant.media[1].width }}"
height="{{ card_variant.media[1].height }}"
>
{%- endif -%}
</div>
</div>
{%- endif -%}
<div class="card__content">
<div class="card__information">
<h3
class="card__heading"
{% if card_variant.featured_media == null and settings.card_style == 'standard' %}
id="title-{{ section_id }}-{{ card_variant.id }}"
{% endif %}
>
<a
href="{{ card_variant.url }}"
id="StandardCardNoMediaLink-{{ section_id }}-{{ card_variant.id }}"
class="full-unstyled-link"
aria-labelledby="StandardCardNoMediaLink-{{ section_id }}-{{ card_variant.id }} NoMediaStandardBadge-{{ section_id }}-{{ card_variant.id }}"
>
{{ card_variant.product.title | escape }} - {{ variant_option }}
</a>
</h3>
</div>
<div class="card__badge {{ settings.badge_position }}">
{%- if card_variant.available == false -%}
<span
id="NoMediaStandardBadge-{{ section_id }}-{{ card_variant.id }}"
class="badge badge--bottom-left color-{{ settings.sold_out_badge_color_scheme }}"
>
{{- 'products.product.sold_out' | t -}}
</span>
{%- elsif card_variant.compare_at_price > card_variant.price and card_variant.available -%}
<span
id="NoMediaStandardBadge-{{ section_id }}-{{ card_variant.id }}"
class="badge badge--bottom-left color-{{ settings.sale_badge_color_scheme }}"
>
{{- 'products.product.on_sale' | t -}}
</span>
{%- endif -%}
</div>
</div>
</div>
<div class="card__content">
<div class="card__information">
<h3
class="card__heading{% if card_variant.featured_media or settings.card_style == 'standard' %} h5{% endif %}"
{% if card_variant.featured_media or settings.card_style == 'card' %}
id="title-{{ section_id }}-{{ card_variant.id }}"
{% endif %}
>
<a
href="{{ card_variant.url }}"
id="CardLink-{{ section_id }}-{{ card_variant.id }}"
class="full-unstyled-link"
aria-labelledby="CardLink-{{ section_id }}-{{ card_variant.id }} Badge-{{ section_id }}-{{ card_variant.id }}"
>
{{ card_variant.product.title | escape }} - {{ variant_option }}
</a>
</h3>
<div class="card-information">
{%- if show_vendor -%}
<span class="visually-hidden">{{ 'accessibility.vendor' | t }}</span>
<div class="caption-with-letter-spacing light">{{ card_variant.vendor }}</div>
{%- endif -%}
<span class="caption-large light">{{ block.settings.description | escape }}</span>
{%- if show_rating and card_variant.metafields.reviews.rating.value != blank -%}
{% liquid
assign rating_decimal = 0
assign decimal = card_variant.metafields.reviews.rating.value.rating | modulo: 1
if decimal >= 0.3 and decimal <= 0.7
assign rating_decimal = 0.5
elsif decimal > 0.7
assign rating_decimal = 1
endif
%}
<div
class="rating"
role="img"
aria-label="{{ 'accessibility.star_reviews_info' | t: rating_value: card_variant.metafields.reviews.rating.value, rating_max: card_variant.metafields.reviews.rating.value.scale_max }}"
>
<span
aria-hidden="true"
class="rating-star"
style="--rating: {{ card_variant.metafields.reviews.rating.value.rating | floor }}; --rating-max: {{ card_variant.metafields.reviews.rating.value.scale_max }}; --rating-decimal: {{ rating_decimal }};"
></span>
</div>
<p class="rating-text caption">
<span aria-hidden="true">
{{- card_variant.metafields.reviews.rating.value }} /
{{ card_variant.metafields.reviews.rating.value.scale_max -}}
</span>
</p>
<p class="rating-count caption">
<span aria-hidden="true">({{ card_variant.metafields.reviews.rating_count }})</span>
<span class="visually-hidden">
{{- card_variant.metafields.reviews.rating_count }}
{{ 'accessibility.total_reviews' | t -}}
</span>
</p>
{%- endif -%}
{% render 'price', product: card_variant, price_class: '', show_compare_at_price: true %}
{%- if card_variant.quantity_price_breaks_configured? -%}
<div class="card__information-volume-pricing-note">
<span class="caption">{{ 'products.product.volume_pricing.note' | t }}</span>
</div>
{%- endif -%}
</div>
</div>
{%- if show_quick_add -%}
<div class="quick-add no-js-hidden">
{%- liquid
assign product_form_id = 'quick-add-' | append: section_id | append: card_variant.id
assign qty_rules = false
if card_variant.selected_or_first_available_variant.quantity_rule.min > 1 or card_variant.selected_or_first_available_variant.quantity_rule.max != null or card_variant.selected_or_first_available_variant.quantity_rule.increment > 1
assign qty_rules = true
endif
-%}
{%- if card_variant.variants.size > 1 or qty_rules -%}
<modal-opener data-modal="#QuickAdd-{{ card_variant.id }}">
<button
id="{{ product_form_id }}-submit"
type="submit"
name="add"
class="quick-add__submit button button--full-width button--secondary{% if horizontal_quick_add %} card--horizontal__quick-add animate-arrow{% endif %}"
aria-haspopup="dialog"
aria-labelledby="{{ product_form_id }}-submit title-{{ section_id }}-{{ card_variant.id }}"
data-product-url="{{ card_variant.url }}"
>
{{ 'products.product.choose_options' | t }}
{%- if horizontal_quick_add -%}
<span class="icon-wrap">{% render 'icon-arrow' %}</span>
{%- endif -%}
<div class="loading-overlay__spinner hidden">
<svg
aria-hidden="true"
focusable="false"
class="spinner"
viewBox="0 0 66 66"
xmlns="http://www.w3.org/2000/svg"
>
<circle class="path" fill="none" stroke-width="6" cx="33" cy="33" r="30"></circle>
</svg>
</div>
</button>
</modal-opener>
<quick-add-modal id="QuickAdd-{{ card_variant.id }}" class="quick-add-modal">
<div
role="dialog"
aria-label="{{ 'products.product.choose_product_options' | t: product_name: card_variant.product.title | escape }} - {{ variant_option }}"
aria-modal="true"
class="quick-add-modal__content global-settings-popup"
tabindex="-1"
>
<button
id="ModalClose-{{ card_variant.id }}"
type="button"
class="quick-add-modal__toggle"
aria-label="{{ 'accessibility.close' | t }}"
>
{% render 'icon-close' %}
</button>
<div id="QuickAddInfo-{{ card_variant.id }}" class="quick-add-modal__content-info"></div>
</div>
</quick-add-modal>
{%- else -%}
<product-form data-section-id="{{ section.id }}">
{%- form 'product',
card_variant,
id: product_form_id,
class: 'form',
novalidate: 'novalidate',
data-type: 'add-to-cart-form'
-%}
<input
type="hidden"
name="id"
value="{{ card_variant.selected_or_first_available_variant.id }}"
class="product-variant-id"
disabled
>
<button
id="{{ product_form_id }}-submit"
type="submit"
name="add"
class="quick-add__submit button button--full-width button--secondary{% if horizontal_quick_add %} card--horizontal__quick-add{% endif %}"
aria-haspopup="dialog"
aria-labelledby="{{ product_form_id }}-submit title-{{ section_id }}-{{ card_variant.id }}"
aria-live="polite"
data-sold-out-message="true"
{% if card_variant.selected_or_first_available_variant.available == false %}
disabled
{% endif %}
>
<span>
{%- if card_variant.selected_or_first_available_variant.available -%}
{{ 'products.product.add_to_cart' | t }}
{%- else -%}
{{ 'products.product.sold_out' | t }}
{%- endif -%}
</span>
<span class="sold-out-message hidden">
{{ 'products.product.sold_out' | t }}
</span>
{%- if horizontal_quick_add -%}
<span class="icon-wrap">{% render 'icon-plus' %}</span>
{%- endif -%}
<div class="loading-overlay__spinner hidden">
<svg
aria-hidden="true"
focusable="false"
class="spinner"
viewBox="0 0 66 66"
xmlns="http://www.w3.org/2000/svg"
>
<circle class="path" fill="none" stroke-width="6" cx="33" cy="33" r="30"></circle>
</svg>
</div>
</button>
{%- endform -%}
</product-form>
{%- endif -%}
</div>
{%- endif -%}
<div class="card__badge {{ settings.badge_position }}">
{%- if card_variant.available == false -%}
<span
id="Badge-{{ section_id }}-{{ card_variant.id }}"
class="badge badge--bottom-left color-{{ settings.sold_out_badge_color_scheme }}"
>
{{- 'products.product.sold_out' | t -}}
</span>
{%- elsif card_variant.compare_at_price > card_variant.price and card_variant.available -%}
<span
id="Badge-{{ section_id }}-{{ card_variant.id }}"
class="badge badge--bottom-left color-{{ settings.sale_badge_color_scheme }}"
>
{{- 'products.product.on_sale' | t -}}
</span>
{%- endif -%}
</div>
</div>
</div>
</div>
{%- else -%}
<div class="card-wrapper product-card-wrapper underline-links-hover">
<div
class="
card card--{{ settings.card_style }}
{% if extend_height %} card--extend-height{% endif %}
{% if settings.card_style == 'card' %} color-{{ settings.card_color_scheme }} gradient{% endif %}
"
style="--ratio-percent: 100%;"
>
<div
class="card__inner{% if settings.card_style == 'standard' %} color-{{ settings.card_color_scheme }} gradient{% endif %} ratio"
style="--ratio-percent: 100%;"
>
<div class="card__media">
<div class="media media--transparent">
{%- if placeholder_image -%}
{{ placeholder_image | placeholder_svg_tag: 'placeholder-svg' }}
{%- else -%}
{{ 'product-apparel-2' | placeholder_svg_tag: 'placeholder-svg' }}
{% endif %}
</div>
</div>
</div>
<div class="card__content">
<div class="card__information">
<h3 class="card__heading card__heading--placeholder{% if settings.card_style == 'standard' %} h5{% endif %}">
<a role="link" aria-disabled="true" class="full-unstyled-link">
{{ 'onboarding.product_title' | t }}
</a>
</h3>
<div class="card-information">
{%- if show_vendor -%}
<span class="visually-hidden">{{ 'accessibility.vendor' | t }}</span>
<div class="caption-with-letter-spacing light">{{ 'products.product.vendor' | t }}</div>
{%- endif -%}
{% render 'price', show_compare_at_price: true %}
</div>
</div>
</div>
</div>
</div>
{%- endif -%}
Step – 3: Edit the file main-collection-product-grid.liquid
Click on the search bar and search for the file main-collection-product-grid.
Scroll to the line 162 and replace this chunk of code below (from line 162 to 180 of the file)
<li
class="grid__item{% if settings.animations_reveal_on_scroll %} scroll-trigger animate--slide-in{% endif %}"
{% if settings.animations_reveal_on_scroll %}
data-cascade
style="--animation-order: {{ forloop.index }};"
{% endif %}
>
{% render 'card-product',
card_product: product,
media_aspect_ratio: section.settings.image_ratio,
image_shape: section.settings.image_shape,
show_secondary_image: section.settings.show_secondary_image,
show_vendor: section.settings.show_vendor,
show_rating: section.settings.show_rating,
lazy_load: lazy_load,
show_quick_add: section.settings.enable_quick_add,
section_id: section.id
%}
</li>
With this new code:
{%- liquid
assign option_chosen = "Color"
assign option_index = ''
for option in product.options
if option_chosen == option
assign option_index = forloop.index0
break
endif
endfor
-%}
{%- if option_index == '' -%}
<li
class="grid__item{% if settings.animations_reveal_on_scroll %} scroll-trigger animate--slide-in{% endif %}"
{% if settings.animations_reveal_on_scroll %}
data-cascade
style="--animation-order: {{ forloop.index }};"
{% endif %}
>
{% render 'card-product',
card_product: product,
media_aspect_ratio: section.settings.image_ratio,
image_shape: section.settings.image_shape,
show_secondary_image: section.settings.show_secondary_image,
show_vendor: section.settings.show_vendor,
show_rating: section.settings.show_rating,
lazy_load: lazy_load,
show_quick_add: section.settings.enable_quick_add,
section_id: section.id
%}
</li>
{%- else -%}
{% assign displayed_values = "" %}
{% for variant in product.variants %}
{%- liquid
assign variant_option = variant.options[option_index]
assign valueIsDisplayed = false
for value in displayed_values
if value == variant_option
assign valueIsDisplayed = true
break
endif
endfor
-%}
{% unless valueIsDisplayed %}
{%- assign variant_option_arr = variant_option | append: ';' | split: ';' -%}
{% assign displayed_values = displayed_values | concat: variant_option_arr %}
<li
class="grid__item{% if settings.animations_reveal_on_scroll %} scroll-trigger animate--slide-in{% endif %}"
{% if settings.animations_reveal_on_scroll %}
data-cascade
style="--animation-order: {{ forloop.index }};"
{% endif %}
>
{% render 'card-variant',
card_variant: variant,
variant_option: variant_option,
media_aspect_ratio: section.settings.image_ratio,
image_shape: section.settings.image_shape,
show_secondary_image: section.settings.show_secondary_image,
show_vendor: section.settings.show_vendor,
show_rating: section.settings.show_rating,
lazy_load: lazy_load,
show_quick_add: section.settings.enable_quick_add,
section_id: section.id
%}
</li>
{% endunless %}
{% endfor %}
{%- endif -%}
We will show each color variant of the product but if you want to show the featured images of another option, you can do that by changing the value of the variable “option_chosen”.
assign option_chosen = "Color"
For example, if you want to show each size instead of each color, replace “Color” with “Size”.
assign option_chosen = "Size"
Once you are satisfied with the changes you can publish your theme copy.
Explaining the logic of the Code (For developers)
So if you are still reading it means you are interested in understanding how the code works, so let’s go over its logic with simplified code since the code above is quite complexe to break down.
Collections pages use for loops to display the products, something like this:
<ul>
{% for product in collection.products %}
<li>
<img src="{{ product.featured_image | img_url: 'medium' }}" alt="{{ product.title }}">
<h3>{{ product.title }}</h3>
<p>{{ product.price | money }}</p>
</li>
{% endfor %}
</ul>
In the Dawn theme, all the info related to the product are contained in the card-product snippet
<ul>
{% for product in collection.products %}
<li>
{% render 'card-product', card_product: product %}
</li>
{% endfor %}
</ul>
So what we want is to render each variant separatly, we access the variant
object inside the product
object and we loop over all the variants, and create a card-variant snippet from the card-product snippet that displays the info related to the variant instead like the price, I’m not going to detail it since it’s pretty simple you will just need to replace the variables like product.price
with variant.price
for example.
<ul>
{% for product in collection.products %}
{% for variant in product.variants %}
<li>
{% render 'card-variant', variant_product: variant %}
</li>
{% endfor %}
{% endfor %}
</ul>
Ok so far so good. Now let’s say we have a product with 2 options Color (green, red and blue) and Size (S,M and L) and we want to display all the colors, the issue that we are going to run into is that we have 9 different variants shown, we only want to show the 3 variant colors.
<ul>
{% for product in collection.products %}
{% assign displayed_values = "" %}
{% for variant in product.variants %}
{%- liquid
assign variant_option = variant.options[option_index]
assign valueIsDisplayed = false
for value in displayed_values
if value == variant_option
assign valueIsDisplayed = true
break
endif
endfor
-%}
{% unless valueIsDisplayed %}
{%- assign variant_option_arr = variant_option | append: ';' | split: ';' -%}
{% assign displayed_values = displayed_values | concat: variant_option_arr %}
<li>
{% render 'card-variant', variant_product: variant %}
</li>
{% endunless %}
{% endfor %}
{% endfor %}
</ul>
We use the displayed_values
array to track the colors that were previously displayed so we only display variants that aren’t contained in that variable, we do that using our unless
condition.
Our final issue is that some products don’t have multiple colors and we don’t know if Color is the first option or the second.
So we can loop over the option if we find the “Color” option, then we render the card-variant else
we render the regular card-product snippet, this is how we end up with this code.
{%- liquid
assign option_chosen = "Color"
assign option_index = ''
for option in product.options
if option_chosen == option
assign option_index = forloop.index0
break
endif
endfor
-%}
{%- if option_index == '' -%}
<li
class="grid__item{% if settings.animations_reveal_on_scroll %} scroll-trigger animate--slide-in{% endif %}"
{% if settings.animations_reveal_on_scroll %}
data-cascade
style="--animation-order: {{ forloop.index }};"
{% endif %}
>
{% render 'card-product',
card_product: product,
media_aspect_ratio: section.settings.image_ratio,
image_shape: section.settings.image_shape,
show_secondary_image: section.settings.show_secondary_image,
show_vendor: section.settings.show_vendor,
show_rating: section.settings.show_rating,
lazy_load: lazy_load,
show_quick_add: section.settings.enable_quick_add,
section_id: section.id
%}
</li>
{%- else -%}
{% assign displayed_values = "" %}
{% for variant in product.variants %}
{%- liquid
assign variant_option = variant.options[option_index]
assign valueIsDisplayed = false
for value in displayed_values
if value == variant_option
assign valueIsDisplayed = true
break
endif
endfor
-%}
{% unless valueIsDisplayed %}
{%- assign variant_option_arr = variant_option | append: ';' | split: ';' -%}
{% assign displayed_values = displayed_values | concat: variant_option_arr %}
<li
class="grid__item{% if settings.animations_reveal_on_scroll %} scroll-trigger animate--slide-in{% endif %}"
{% if settings.animations_reveal_on_scroll %}
data-cascade
style="--animation-order: {{ forloop.index }};"
{% endif %}
>
{% render 'card-variant',
card_variant: variant,
variant_option: variant_option,
media_aspect_ratio: section.settings.image_ratio,
image_shape: section.settings.image_shape,
show_secondary_image: section.settings.show_secondary_image,
show_vendor: section.settings.show_vendor,
show_rating: section.settings.show_rating,
lazy_load: lazy_load,
show_quick_add: section.settings.enable_quick_add,
section_id: section.id
%}
</li>
{% endunless %}
{% endfor %}
{%- endif -%}