Want personalized guidance adding this to your store?
Check out our Insiders community: https://www.skool.com/the-prompted
Members of The Prompted community receive a detailed store audit, 1-on-1 guidance for implementing new features, and access to an exclusive theme. You'll also get marketing support, the same tactics we use to spend over $100k/mo on Meta Ads.
---
In this tutorial, we’re looking at product variant swatches again.
While we’re still waiting for the native swatches to finally be available, we have to use some workarounds like what I’ll be showing you in this customization. And whether or not you installed our previous version of our swatch customization, I'll show you step by step how to add the swatches to your store, without needing to pay for an app or hire a developer.
Compatible Themes: This code should work on all free v14 Shopify themes (Dawn, Refresh, Craft, Studio, Publisher, Crave, Origin, Taste, Colorblock, Sense, Ride, Spotlight). For v15 themes, you will need to use this tutorial instead.
Links:
- Use these swatches to combine multiple products together as variants
- Add swatches to your collection page
- Updated swatch tutorial compatible with v15 themes
Upload Image Files
Upload your Swatch image files
Left Menu Bar: Content —> Files
Create Metaobjects and Metafields
Create Metaobject “Variant Swatch Map”
Make sure handles of the metaobject and fields match those shown in the video since the code references those exact handles.
- Field 1: Title
- Regular Expression:
^[a-zA-Z0-9_-]+$
(pattern to match - alphanumeric with - and _)
- Regular Expression:
- Field 2: Variant Images JSON
- The JSON will have the following schema defined:
{ "$id": "variants_images.schema.json", "$schema": "<http://json-schema.org/draft-07/schema#>", "title": "Variants List", "description": "A list of variant swatches", "type": "array", "items": { "type": "object", "properties": { "variant_name": { "type": "string", "description": "The variant option name." }, "variant_value": { "type": "string", "description": "The variant option value." }, "variant_swatch": { "type": "string", "description": "The filename or URL of the image representing the color." }, "variant_hex": { "type": "string", "description": "The HEX value or description of the color." } }, "required": [ "variant_name", "variant_value" ] } }
Create Metaobject entry called “variant-swatch-mapping”
Add JSON entries that reference your uploaded images files. For example:
[
{
"variant_name": "Material",
"variant_value": "Cotton",
"variant_swatch": "cotton.jpg"
},
{
"variant_name": "Material",
"variant_value": "Polyester",
"variant_swatch": "polyester.jpg"
},
{
"variant_name": "Color",
"variant_value": "Blue",
"variant_swatch": "bluerose.jpg"
},
{
"variant_name": "Color",
"variant_value": "Green",
"variant_swatch": "greenimage.jpg"
},
{
"variant_name": "Color",
"variant_value": "Black",
"variant_swatch": "",
"variant_hex": "#000000"
},
{
"variant_name": "Color",
"variant_value": "Red",
"variant_swatch": "",
"variant_hex": "#FF0000"
}
]
Create metafield Variant Swatch Map Override
Use type Variant Swatch Map.
This metafield is for any products you wish to have product-specific swatch mapping. It will override the default metaobject entry variant-swatch-mapping
Edit Theme Code
Edit main-product.liquid schema
Found under the variant picker block
{
"type": "header",
"content": "Custom Swatch"
},
{
"id": "swatch_shape_custom",
"label": "Swatch (custom)",
"type": "select",
"info": "Variant picker style must be Pills to use the custom swatches",
"options": [
{
"value": "circle",
"label": "t:sections.main-product.blocks.variant_picker.settings.swatch_shape.options__1.label"
},
{
"value": "square",
"label": "t:sections.main-product.blocks.variant_picker.settings.swatch_shape.options__2.label"
},
{
"value": "none",
"label": "t:sections.main-product.blocks.variant_picker.settings.swatch_shape.options__3.label"
}
],
"default": "none"
},
{
"type": "text",
"id": "variant_swatch_metaobject",
"label": "Default Variant Swatch Map Metaobject",
"default": "variant-swatch-mapping",
"info": "Can be overridden with product metafield"
},
{
"type": "range",
"id": "swatch_size",
"min": 30,
"max": 60,
"step": 1,
"unit": "px",
"label": "Swatch Size",
"default": 40
}
Edit product-variant-picker.liquid
Assign variables and update fieldset around the loop:
{%- for option in product.options_with_values -%}
We will only edit the picker_type “button”. We will not edit the picker_type “swatch” as this hasn’t been fully rolled out yet and we don’t want to modify it.
Code before the loop code {%- for option in product.options_with_values -%}
{% if product.metafields.custom.variant_swatch_map_override.value %}
{% assign target_entry = product.metafields.custom.variant_swatch_map_override.value %}
{% else %}
{% assign entry_title = block.settings.variant_swatch_metaobject %}
{% assign target_entry = nil %}
{% for entry in shop.metaobjects.variant_swatch_map.values %}
{% if entry.title == entry_title %}
{% assign target_entry = entry %}
{% break %}
{% endif %}
{% endfor %}
{% endif %}
{% if target_entry %}
{% assign variant_images_data = target_entry.variant_images_json %}
{% else %}
{% assign variant_images_data = nil %}
{% endif %}
Add this code below {%- elsif picker_type == 'button' -%}
{% if block.settings.swatch_shape_custom == "none" %}
... existing code here ...
{% else %}
<style>
:root {
--swatch-size: {{ block.settings.swatch_size }}px;
--swatch-border-radius: {% if block.settings.swatch_shape_custom == 'circle' %}50%{% else %}0%{% endif %};
}
</style>
{{ 'component-product-variant-swatch-custom.css' | asset_url | stylesheet_tag }}
{% assign variant_options_images = variant_images_data.value | where: "variant_name", option.name %}
<fieldset class="js product-form__input product-form__input--pill">
{% if variant_options_images.size > 0 %}
<legend class="form__label">
{{ option.name }}:
<span id="selected{{ option.name }}">{{ option.selected_value }}</span>
</legend>
{% render 'product-variant-swatch-custom', product: product, option: option, variant_images_data: variant_options_images %}
{% else %}
<legend class="form__label">{{ option.name }}</legend>
{% render 'product-variant-options',
product: product,
option: option,
block: block,
picker_type: picker_type
%}
{% endif %}
</fieldset>
{% endif %}
Add js at end of the file
<script src="{{ 'product-variant-selection-custom.js' | asset_url }}" defer="defer"></script>
Create liquid snippet product-variant-swatch-custom.liquid
{% comment %}
Description:
Renders product variant swatch options based on image URLs. Defaults to buttons if no image URL is present.
This is generalized for any variant type.
Accepts:
- product: {Object} product object.
- option: {Object} current product_option object.
- variant_images_data: list of variant images
Usage:
{% render '**product-variant-swatch-custom**',
product: product,
option: option,
variant_images_data: variant_images_data
%}
{% endcomment %}
{% assign base_store_files_url = '//' | append: shop.permanent_domain | append: '/cdn/shop/files/' %}
{%- liquid
assign product_form_id = 'product-form-' | append: section.id
-%}
<div class = "product-form-swatch__variants">
{% for value in option.values %}
{% assign variant_image_url = nil %}
{% assign option_disabled = true %}
{% for variant in product.variants %}
{% case option.position %}
{% when 1 %}
{% if variant.option1 == value %}
{% assign variant_image_url = variant.featured_media | img_url: '100x100' %}
{% if variant.available %}
{% assign option_disabled = false %}
{% endif %}
{% endif %}
{% when 2 %}
{% if variant.option2 == value and variant.option1 == product.selected_or_first_available_variant.option1 %}
{% assign variant_image_url = variant.featured_media | img_url: '100x100' %}
{% if variant.available %}
{% assign option_disabled = false %}
{% endif %}
{% endif %}
{% when 3 %}
{% if variant.option3 == value and variant.option1 == product.selected_or_first_available_variant.option1 and variant.option2 == product.selected_or_first_available_variant.option2 %}
{% assign variant_image_url = variant.featured_media | img_url: '100x100' %}
{% if variant.available %}
{% assign option_disabled = false %}
{% endif %}
{% endif %}
{% endcase %}
{% if variant_image_url %}
{% assign swatch_found = false %}
{% for item in variant_images_data %}
{% if item.variant_value == value %}
{% if item.variant_swatch != blank %}
{% assign variant_image_url = base_store_files_url | append: item.variant_swatch %}
{% assign swatch_found = true %}
{% elsif item.variant_hex %}
{% assign hex_color = item.variant_hex | replace: '#', '%23' %}
{% assign svg = '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect width="100%" height="100%" fill="' | append: hex_color | append: '" /></svg>' %}
{% assign encoded_svg = svg | replace: '"', '%22' | replace: '<', '%3C' | replace: '>', '%3E' %}
{% assign variant_image_url = 'data:image/svg+xml;charset=utf-8,' | append: encoded_svg %}
{% assign swatch_found = true %}
{% endif %}
{% if swatch_found %}
{% break %}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
<div class="product-form__swatch">
<input
type="radio"
id="{{ section.id }}-{{ option.position }}-{{ forloop.index0 }}"
name="{{ option.name }}"
value="{{ value | escape }}"
form="{{ product_form_id }}"
data-product-id="{{ product.id }}"
data-image-url="{{ variant_image_url }}"
{% if option.selected_value == value %}
checked
{% endif %}
{% if option_disabled %}
class="disabled"
{% endif %}
>
<label for="{{ section.id }}-{{ option.position }}-{{ forloop.index0 }}" style="background-image: url('{{ variant_image_url }}');">
<span class="visually-hidden">{{ 'products.product.variant_sold_out_or_unavailable' | t }}</span>
</label>
</div>
{% endfor %}
</div>
Create asset component-product-variant-swatch-custom.css
.product-form-swatch__variants {
display: flex;
flex-wrap: wrap;
}
.product-form__swatch {
display: inline-block;
margin-right: 5px;
}
.product-form__swatch input {
display: none;
}
.product-form__swatch label {
display: block;
width: var(--swatch-size);
height: var(--swatch-size);
border: 1px solid #777 !important;
border-radius: var(--swatch-border-radius) !important;
background-size: cover;
cursor: pointer;
transition: border-color 0.3s ease;
padding: 1rem !important;
}
.product-form__swatch label:hover {
border-color: #333 !important;
}
.product-form__swatch input:checked + label {
border-color: #333 !important;
border-width: 2px !important;
box-shadow: inset 0 0 0 1px #fff;
}
.product-form__swatch input.disabled + label {
opacity: 0.5;
}
.product-form__swatch input.disabled + label::after {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: linear-gradient(to bottom right, transparent 45%, rgba(255, 0, 0, 0.6) 50%, transparent 55%);
pointer-events: none;
}
Create asset product-variant-selection-custom.js
document.addEventListener('DOMContentLoaded', function() {
var variantInputs = document.querySelectorAll('.product-form__swatch input[type="radio"], .product-form__swatch button');
function updateVariantDisplay(e) {
console.log('Variant changed'); // Debugging statement
var selectedValue = e.target.tagName === 'BUTTON' ? e.target.getAttribute('data-value') : e.target.value;
var optionName = e.target.name;
var variantDisplay = document.getElementById('selected' + optionName);
if(variantDisplay) {
variantDisplay.textContent = selectedValue;
}
}
variantInputs.forEach(function(input) {
input.addEventListener('change', updateVariantDisplay);
if(input.tagName === 'BUTTON') {
input.addEventListener('click', updateVariantDisplay);
}
});
});