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.
---
We’re back with version 4 of our product variant swatches to align with Shopify's new v15 themes, such as Dawn, Sense, Refresh, Origin, and so on.
Compatible Themes: This code should work on all free Shopify v15 themes (Dawn, Refresh, Craft, Studio, Publisher, Crave, Origin, Taste, Colorblock, Sense, Ride, Spotlight). Note this will not work on previous (v14 or older) themes. For v14, use this tutorial instead.
Links:
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 %}
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 %}
{%- capture input_id -%}
{{ section.id }}-{{ option.position }}-{{ forloop.index0 }}
{%- endcapture -%}
{%- capture input_name -%}
{{ option.name }}-{{ option.position }}
{%- endcapture -%}
<div class="product-form__swatch">
<input
type="radio"
id="{{ input_id }}"
name="{{ input_name }}"
value="{{ value | escape }}"
form="{{ product_form_id }}"
data-product-url="{{ product.url }}"
data-option-value-id="{{ value.id }}"
data-image-url="{{ variant_image_url }}"
{% if option.selected_value == value %}
checked
{% endif %}
{% if option_disabled %}
class="disabled"
{% endif %}
>
<label for="{{ input_id }}" style="background-image: url('{{ variant_image_url }}');">
<span class="visually-hidden">{{ value | escape }}</span>
<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;
}
Edit product-info.js
Add call to method in connectedCallback()
this.initializeCustomSwatches();
Add new methods
initializeCustomSwatches() {
const swatchInputs = this.querySelectorAll('.product-form__swatch input[type="radio"], .product-form__swatch button');
swatchInputs.forEach(input => {
input.addEventListener('change', this.handleSwatchChange.bind(this));
if (input.tagName === 'BUTTON') {
input.addEventListener('click', this.handleSwatchChange.bind(this));
}
});
}
handleSwatchChange(event) {
const selectedValue = event.target.tagName === 'BUTTON' ? event.target.getAttribute('data-value') : event.target.value;
const optionName = event.target.name;
const variantDisplay = this.querySelector(`#selected${optionName}`);
if (variantDisplay) {
variantDisplay.textContent = selectedValue;
}
this.onVariantChange();
}