Go to Settings → Custom data → Metaobjects and click
Add definition. Name it exactly:
Type Variant Product
Add two fields to this definition:
· Heading — Single line text (the shared model name, e.g. The Fred Perry Shirt)
· Products — List of products (every colour variant of that model)
heading value is converted to a handle using downcase | replace: ' ', '-' in Liquid. Shopify usually auto-generates a matching handle when you save the entry — double-check it matches.
Go to Content → Metaobjects → Type Variant Product and click Add entry.
· heading: The shared model name (e.g. The Fred Perry Shirt)
· products: Select every product that is a colour variant of this model
Repeat for each product group in your store. One entry = one set of swatches.
Go to Online Store → Themes → Edit Code → Snippets and create a new file. Name it exactly:
product-type-variant.liquid
Paste the full snippet below into that file and save:
<fieldset class="product-form__input product-form__input--block"> {% assign color = product.variants.first.option1 %} <div class="form__label"> {{ product.options_with_values[0].name }} <span class="form__label__value">{{ color }}</span> </div> <div class="product-type-variant__container"> {% assign metaObjectVariant = metaobjects.type_variant_product.values %} {% paginate metaObjectVariant by 250 %} {% assign typeVariantFound = false %} {% for typeVariant in metaobjects.type_variant_product.values %} {% assign typeVariantHeading = '' %} {% for typeVariantProduct in typeVariant.products.value %} {% if product.id == typeVariantProduct.id %} {% assign typeVariantHeading = typeVariant.heading.value %} {% assign typeVariantFound = true %} {% break %} {% endif %} {% endfor %} {% if typeVariantFound %} {% assign typeVariantHandle = typeVariantHeading | downcase | replace: ' ', '-' %} {% assign brand_images = shop.metaobjects["type_variant_product"][typeVariantHandle] %} {% assign swatch_count = brand_images.products.value | size %} <div class="product-type-variant__clap{% if swatch_count > 6 %} product-type-variant__clap--collapsed{% endif %}" data-clap> {% if swatch_count > 6 %} <div class="product-type-variant__chevron-wrap" data-clap-toggle> <span class="svg-wrapper">{{- 'icon-caret.svg' | inline_asset_content -}}</span> </div> {% endif %} <div class="product-type-variant__swatches"> {%- comment -%} Active swatch first {%- endcomment -%} {% for productType in brand_images.products.value %} {% if productType.handle == product.handle %} <a href="{{ productType.url }}" class="product-type-variant__product product-type-variant__product--active"> {{ productType.featured_image | image_url: width: 500 | image_tag: widths: '50, 75, 100, 150, 200, 300, 400, 500', alt: productType.title }} </a> {% endif %} {% endfor %} {%- comment -%} Then all others {%- endcomment -%} {% for productType in brand_images.products.value %} {% unless productType.handle == product.handle %} <a href="{{ productType.url }}" class=" product-type-variant__product {% if productType.selected_or_first_available_variant.available == false %}product-type-variant__product--sold-out{% endif %} "> {{ productType.featured_image | image_url: width: 500 | image_tag: widths: '50, 75, 100, 150, 200, 300, 400, 500', alt: productType.title }} </a> {% endunless %} {% endfor %} </div> {% if swatch_count > 6 %} <div class="product-type-variant__gradient"></div> {% endif %} </div> {% break %} {% endif %} {% endfor %} <div class="varian-type-pagination" style="display:none"> {{- paginate | default_pagination -}} </div> {% endpaginate %} </div> </fieldset>
{% if metaObjectVariant.count <= 250 %} wrapper has been removed. The {% paginate %} tag handles the 250-entry fetch on its own, so the extra conditional was unnecessary.
Paste these styles inside a <style> block at the bottom of the snippet, or add them to your theme's base.css:
.product-type-variant__clap { position: relative; }
.product-type-variant__swatches {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.product-type-variant__clap--collapsed .product-type-variant__swatches {
max-height: 150px;
overflow: hidden;
}
/* Chevron — top right corner */
.product-type-variant__chevron-wrap {
position: absolute;
top: 0;
right: 0;
z-index: 2;
cursor: pointer;
padding: 2px;
line-height: 0;
}
.product-type-variant__chevron-wrap svg {
width: 20px;
height: 20px;
transition: transform 0.2s ease;
}
.product-type-variant__clap:not(.product-type-variant__clap--collapsed)
.product-type-variant__chevron-wrap svg {
transform: rotate(180deg);
}
/* Gradient fade — visible only when collapsed */
.product-type-variant__gradient {
display: none;
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 65px;
background: linear-gradient(to bottom, transparent, #ffffff);
cursor: pointer;
}
.product-type-variant__clap--collapsed .product-type-variant__gradient {
display: block;
}
.product-type-variant__product {
height: 115px;
overflow: hidden;
}
.product-type-variant__container img {
width: 100% !important;
height: 100% !important;
object-fit: cover;
}
/* Active swatch */
.product-type-variant__product--active {
border: 1.5px solid #000;
}
/* Sold-out state */
.product-type-variant__product--sold-out { opacity: 0.45; }
@media (max-width: 550px) {
.product-type-variant__chevron-wrap { top: -30px; }
}
Paste this at the bottom of the snippet inside a <script> tag. It handles the chevron collapse/expand and the gradient click-to-reveal:
// Chevron — expands AND collapses document.querySelector('.product-type-variant__chevron-wrap').addEventListener('click', function(e) { const clap = e.target.closest('[data-clap]'); clap.classList.toggle('product-type-variant__clap--collapsed'); }); // Gradient — expand only document.querySelector('.product-type-variant__gradient').addEventListener('click', function(e) { const clap = e.target.closest('[data-clap]'); clap.classList.remove('product-type-variant__clap--collapsed'); });
Open product-variant-picker.liquid and find the {%- for option in product.options_with_values -%} loop. Wrap it so the Color option uses your snippet while all other options render normally:
{% for option in product.options_with_values %} {% if option.name == 'Color' %} {% render 'product-type-variant', product: product, option: option, block: block, section: section %} {% else %} {%- liquid assign swatch_count = option.values | map: 'swatch' | compact | size assign picker_type = block.settings.picker_type ...rest of original loop -%} {% endif %} {% endfor %}
Open any product that belongs to a metaobject group and you'll see its
colour siblings rendered as image swatches. If there are more than 6,
they collapse with a gradient fade — click the chevron or the gradient
to expand. Adjust the threshold by changing
swatch_count > 6 in the snippet.
I implement this kind of custom Shopify development daily — no apps, clean code, done fast. Tell me what you need and I'll get it done right.