Case study in showing all colours for a product on a collection page, while still being able to filter by colour, size etc

Short description of issue

A recurring problem with Shopify’s product-variant relationships is showing all colours: this solves it

Reproduction steps

If your store has products which have colours, and underneath those colours they have sizes, you will know what this is

Additional info

A requirement of our online shoe shop was to have the following product setup: styles of shoes have colours, and colours have sizes, so the product variant hierarchy is style → colour → size
Another requirement was that ALL colours should be shown on collection pages - as you will know, by default Shopify only shows one colour per product, which is not ideal
A further requirement was that collections should be filterable by colour and size, which are variant level. However by default, Shopify will show a product if any variant it possesses meets the search criteria. This results in the customer seeing colours for which their selected size is not available.

This was my solution to meet these three requirements. I present it here in case other devs or owners facing the same problem can adapt it for their needs. It is fugly - liquid is a templating language, not a scripting language, so we have to commit some pretty awful sins against normal syntax. Code in comments.

What type of topic is this

Troubleshooting

Just below the portion of the collection section that refreshes when facets are changed - it’s no use up at the top of the file - insert the following code:

{% comment %}
FILTER WORKING START
work out what filters we have to satisfy: tags are product level so no further work is needed, but colour, material and size are variant level and will require extra filtering to disqualify the variants that Shopify wants to show  
{% endcomment %}
{%- liquid

    comment
      initialise the variables we'll need to track what each variant has to satisfy in order to be displayed on the page
    endcomment
  
    assign colour_match_required = false
    assign material_match_required = false
    assign size_match_required = false

    assign colour_match_possibilities = ""
    assign material_match_possibilities = ""
    assign size_match_possibilities = ""

-%}

{% comment %}
  iterate through all active filters and work out what our requirements are - what kind of matches are required, and what possibilities each one has
{% endcomment %}
{%- for filter in collection.filters -%}

    {% for filter_value in filter.active_values %}

        {%- liquid
          
            if filter_value.param_name == "filter.v.m.custom.wildsmith_colour_family"

                assign colour_match_required = true

                assign colour_match_possibilities = colour_match_possibilities | append: filter_value.label | append: "|"

            endif

            if filter_value.param_name == "filter.v.m.custom.wildsmith_material"

                assign material_match_required = true

                assign material_match_possibilities = material_match_possibilities | append: filter_value.label | append: "|"

            endif

            if filter_value.param_name == "filter.v.t.shopify.shoe-size"

                assign size_match_required = true

                assign size_match_possibilities = size_match_possibilities | append: filter_value.label | append: "|"

            endif

        -%}

    {% endfor %}

{%- endfor -%}

{%- liquid

    comment
      turn the strings we created above into arrays, for later matching
    endcomment
  
    assign colour_match_possibilities = colour_match_possibilities | remove_last: "|" 
    assign colour_match_possibilities = colour_match_possibilities | split: "|" 

    assign material_match_possibilities = material_match_possibilities | remove_last: "|" 
    assign material_match_possibilities = material_match_possibilities | split: "|" 

    assign size_match_possibilities = size_match_possibilities | remove_last: "|" 
    assign size_match_possibilities = size_match_possibilities | split: "|"

-%}


{%  comment %}
FILTER WORKING END
{% endcomment %}

{%  comment %}
VARIANT WORKING START
work out what variants we should show - one per colour if the colour qualifies
{% endcomment %}
{%- liquid
  
    assign qualifying_variants = ""
    assign qualifying_product_colours = ""

-%}
{% comment %}
    iterate over products and variants - for each colour-level, we will check the size variants for each one. If all matches are satisifed, the 'winning' variant ID will be saved, for later checking
{% endcomment %}
{%- for product in collection.products -%}

    {%-  liquid

        comment
          tracking variable for which colour we are currently looping over - although from Shopify's admin you'd think that variants are hierarchical, in liquid land it's just a big flat array - but it is at least ordered, so different colours aren't scattered, they are in single blocks
        endcomment
        assign current_colour = ""

    -%}

    {%- for variant in product.variants -%}

        {% comment  %}
        if at least one of the bottom-level size variants for each colour fulfils all the variant-level filter requirements, we can show that entire colour variant in the grid
        it doesn't matter which size we pick - the collection page just needs a colour variant to display in an adapted version of a product card snippet which takes a variant argument to get the appropriate image and colour name
        {% endcomment %}

        {% comment %}
            we are only interested in available variants - if unavailable it can't contribute to the success of the colour-level
        {% endcomment %}
        {%- if variant.available -%}

            {%  comment %}
            The colour has changed block starts here
            {% endcomment %}
            {% if variant.option1 != current_colour %}

                {% assign current_colour = variant.option1 %}

                {% comment  %}
                this is a new colour - it has changed
                {% endcomment %}

                {%- liquid 

                    comment
                      a tracking var so that if a colour qualifies we don't have to carry on checking sub-variants
                    endcomment
                    assign this_colour_qualified = false
                    
                    comment
                    for this colour level variant, work out whether we need to satisfy the three variant level requirements
                    if a particular requirement is not active, it gets a free pass as below
                    endcomment
                    assign current_colour_colour_match_found = false

                    if colour_match_required == false

                        assign current_colour_colour_match_found = true

                    endif

                    assign current_colour_material_match_found = false

                    if material_match_required == false

                        assign current_colour_material_match_found = true

                    endif

                    assign current_colour_size_match_found = false

                    if size_match_required == false

                        assign current_colour_size_match_found = true

                    endif
                    
                -%}

            {% endif %}
            {%  comment %}
            The colour has changed block ends here
            {% endcomment %}

            {% comment %}
            Now we check our requirements for each variant - note that each check is only performed once per colour, if we had a match earlier in the section of the variants array for the current colour, then we don't try again: efficiency!
            {%endcomment %}

            {% comment %}
            BEGIN colour checking for this variant
            foreach each colour that the user ticked in facets, we check if the lower case of that colour is equal to or contained within the lower case colour name of the variant
            if yes for any variant of this colour, then that's a hit
            {%endcomment %}
            {% if colour_match_required and current_colour_colour_match_found == false %}

                {% for colour_match_string in colour_match_possibilities %}

                    {%- liquid 

                        assign dCaseRequiredColourName = colour_match_string | downcase 
                        assign dCaseVariantColourName = variant.option1 | downcase

                        if dCaseVariantColourName == dCaseRequiredColourName or dCaseVariantColourName contains dCaseRequiredColourName

                            assign current_colour_colour_match_found = true

                        endif

                    %}

                {% endfor %}

            {% endif %}
            {% comment %}
            END colour checking for this variant
            {%endcomment %}

            {% comment %}
            BEGIN material checking for this variant
            same checks as colour, but slightly tweaked based on what I know we have as material names within the colour
            {%endcomment %}
            {% if material_match_required and current_colour_material_match_found == false %}

                {% for material_match_string in material_match_possibilities %}

                    {%- liquid 

                        assign dCaseRequiredMaterialName = material_match_string | downcase 
                        assign dCaseVariantMaterialName = variant.option1 | downcase

                        case dCaseRequiredMaterialName

                            when "suede leather"

                                if dCaseVariantMaterialName contains "suede"

                                    assign current_colour_material_match_found = true

                                endif

                            when "leather"

                                if dCaseVariantMaterialName contains "calf" or dCaseVariantMaterialName contains "polished" or dCaseVariantMaterialName contains "patent" or dCaseVariantMaterialName contains "grain"

                                    assign current_colour_material_match_found = true

                                endif

                            else

                                if dCaseVariantMaterialName == dCaseRequiredMaterialName or dCaseVariantMaterialName contains dCaseRequiredMaterialName

                                    assign current_colour_material_match_found = true

                                endif

                        endcase

                    %}

                {% endfor %}

            {% endif %}
            {% comment %}
            END colour checking for this variant
            {%endcomment %}

            {% comment %}
            BEGIN size checking for this variant
            a simple check - if the available variant has the required size string, then bingo
            {%endcomment %}
            {% if size_match_required and current_colour_size_match_found == false %}

                {% for size_match_string in size_match_possibilities %}

                    {%- liquid 

                        assign dCaseRequiredSizeName = size_match_string | downcase 
                        assign dCaseVariantSizeName = variant.option2 | downcase

                        if dCaseVariantSizeName == dCaseRequiredSizeName or dCaseVariantSizeName contains dCaseRequiredSizeName

                            assign current_colour_size_match_found = true

                        endif

                    %}

                {% endfor %}

            {% endif %}
            {% comment %}
            END size checking for this variant
            {% endcomment %}

            {% comment %}
            after all that, do we have all required matches for this colour variant to qualify to be shown?
            {% endcomment %}

            {%- liquid

                if this_colour_qualified == false

                    if current_colour_colour_match_found and current_colour_material_match_found and current_colour_size_match_found

                        comment                        
                            yes, all three matched - we can add this colour to the list of variants we will display
                        endcomment
                        assign this_colour_qualified = true

                        comment
                          this is just a string to help with debugging and concept-proving
                        endcomment
                        assign qualifying_product_colours = qualifying_product_colours | append: product.title | append: " in " | append: variant.option1 | append: ", "

                        comment
                          this is the important bit - for each colour we are able to display, we need a representative variant ID to use later when we're looping through product variants deciding which we should show
                        endcomment
                        assign qualifying_variants = qualifying_variants | append: variant.id | append: "|"

                    endif

                endif
             
            %}
            

        {%- endif -%}

    {%- endfor -%}

{%- endfor -%}
  
{%- liquid
     
    comment
      and turn the string of variant ids into an array
    endcomment
    assign qualifying_variants = qualifying_variants | remove_last: "|"
    assign qualifying_variants = qualifying_variants | split: "|" 
    
-%}

{%  comment %}
VARIANT WORKING END
{% endcomment %}

{% comment %}
this is just a little bit of debugging output
{% endcomment %}
<p>colour_match_possibilities: {{ colour_match_possibilities | json }}<br />
material_match_possibilities: {{ material_match_possibilities | json }}<br />
size_match_possibilities: {{ size_match_possibilities | json }}</p>
<p>qualifying_product_colours: {{ qualifying_product_colours }}<br />
Qualifying variants: {{ qualifying_variants | json }}</p>

Your logic for whether to show a product or not then becomes very simple. Somewhere down in your liquid for the grid section will be something like:

{%- for product in collection.products -%} 
//stuff 
//then something like
{% 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,
skip_styles: skip_card_product_styles,
quick_add: section.settings.quick_add,
section_id: section.id
%}
//more stuff
{% endfor %}

Adapt that to loop over variants too, but ONLY call the snippet if the variant ID is in our list of allowed variants. Remember to turn it into a string, because that’s what your array from above contains.

Also, create a snippet based on card-product that also takes a variant as an argument, and within it get the correct image and title etc. Use that instead of the product-card snippet

{%- for product in collection.products -%} 

	{% for variant in product.variants %}

        {% assign variant_id_str = "" | append: variant.id | append: "" %}

			 {% if qualifying_variants contains variant_id_str %}
			  
				//stuff 
				//then something like
				{% render 'card-product-variant',
				card_product: product,
				card_variant: variant,
				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,
				skip_styles: skip_card_product_styles,
				quick_add: section.settings.quick_add,
				section_id: section.id
				%}
				//more stuff
						
		{% endif %}

	{% endfor %}
						
{% endfor %}

I hope this provides someone else with the solution to a similar problem.

G