In this tutorial, we're using Shopify's native swatches to add product variant swatches to the product and collection page. Unlike our previous methods, this approach leverages Shopify’s built-in functionality for a more seamless and optimized experience.
We show you how to use the product page swatches that are available out of the box in Shopify's themes. After that, we show you how to plug into this existing functionality and adding the swatches to the collection page too.
Compatible Themes: This code should work on all free Shopify themes (Dawn, Refresh, Craft, Studio, Publisher, Crave, Origin, Taste, Colorblock, Sense, Ride, Spotlight).
Activate native swatches
- Assign category metafield color to relevant products
- Connect variant to color metafield
Edit settings_schema.json
Add product card swatch settings
{
"name": "Product Card Swatches",
"settings": [
{
"type": "checkbox",
"label": "Enable Product Card Swatches",
"id": "card_product_swatch_enable",
"default": false
},
{
"type": "header",
"content": "Swatch Format Settings"
},
{
"type": "checkbox",
"id": "card_product_swatch_show_option_name",
"label": "Show Option Name",
"default": false
},
{
"type": "checkbox",
"id": "card_product_swatch_centered",
"label": "Center Swatches",
"default": false
},
{
"id": "card_product_swatch_shape",
"label": "t:sections.main-product.blocks.variant_picker.settings.swatch_shape.label",
"type": "select",
"info": "t:sections.main-product.blocks.variant_picker.settings.swatch_shape.info",
"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"
}
],
"default": "circle"
},
{
"type": "range",
"id": "card_product_swatch_size",
"label": "Swatch Size",
"min": 1,
"max": 3,
"step": 0.1,
"default": 2,
"unit": "rem"
},
{
"type": "range",
"id": "card_product_swatch_margin_top",
"label": "Margin Top",
"min": 0,
"max": 3,
"step": 0.1,
"default": 0.5,
"unit": "rem"
},
{
"type": "range",
"id": "card_product_swatch_margin_right",
"label": "Margin Right",
"min": 0,
"max": 3,
"step": 0.1,
"default": 0.5,
"unit": "rem"
},
{
"type": "range",
"id": "card_product_swatch_margin_bottom",
"label": "Margin Bottom",
"min": 0,
"max": 3,
"step": 0.1,
"default": 0.2,
"unit": "rem"
},
{
"type": "range",
"id": "card_product_swatch_margin_left",
"label": "Margin Left",
"min": 0,
"max": 3,
"step": 0.1,
"default": 0,
"unit": "rem"
},
{
"type": "range",
"id": "card_product_information_padding_top",
"label": "Card Information Padding Top",
"min": 0,
"max": 3,
"step": 0.1,
"default": 1.3,
"unit": "rem"
},
{
"type": "range",
"id": "card_product_information_padding_bottom",
"label": "Card Information Padding Bottom",
"min": 0,
"max": 3,
"step": 0.1,
"default": 1.3,
"unit": "rem"
},
{
"type": "checkbox",
"id": "card_product_swatch_show_from_price",
"label": "Show From Price",
"default": true,
"info": "This will show the 'from' price in the product card if the product has multiple prices."
},
{
"type": "header",
"content": "Product Card Settings"
},
{
"type": "select",
"id": "card_product_swatch_image_ratio",
"options": [
{
"value": "adapt",
"label": "t:sections.main-collection-product-grid.settings.image_ratio.options__1.label"
},
{
"value": "portrait",
"label": "t:sections.main-collection-product-grid.settings.image_ratio.options__2.label"
},
{
"value": "square",
"label": "t:sections.main-collection-product-grid.settings.image_ratio.options__3.label"
}
],
"default": "adapt",
"label": "t:sections.main-collection-product-grid.settings.image_ratio.label"
},
{
"type": "select",
"id": "card_product_swatch_image_shape",
"options": [
{
"value": "default",
"label": "t:sections.all.image_shape.options__1.label"
},
{
"value": "arch",
"label": "t:sections.all.image_shape.options__2.label"
},
{
"value": "blob",
"label": "t:sections.all.image_shape.options__3.label"
},
{
"value": "chevronleft",
"label": "t:sections.all.image_shape.options__4.label"
},
{
"value": "chevronright",
"label": "t:sections.all.image_shape.options__5.label"
},
{
"value": "diamond",
"label": "t:sections.all.image_shape.options__6.label"
},
{
"value": "parallelogram",
"label": "t:sections.all.image_shape.options__7.label"
},
{
"value": "round",
"label": "t:sections.all.image_shape.options__8.label"
}
],
"default": "default",
"label": "t:sections.all.image_shape.label",
"info": "t:sections.all.image_shape.info"
},
{
"type": "checkbox",
"id": "card_product_swatch_show_secondary_image",
"default": false,
"label": "t:sections.main-collection-product-grid.settings.show_secondary_image.label"
},
{
"type": "checkbox",
"id": "card_product_swatch_show_vendor",
"default": false,
"label": "t:sections.main-collection-product-grid.settings.show_vendor.label"
},
{
"type": "checkbox",
"id": "card_product_swatch_show_rating",
"default": false,
"label": "t:sections.main-collection-product-grid.settings.show_rating.label",
"info": "t:sections.main-collection-product-grid.settings.show_rating.info"
},
{
"type": "select",
"id": "card_product_swatch_quick_add",
"default": "none",
"label": "t:sections.main-collection-product-grid.settings.quick_add.label",
"info": "t:sections.main-collection-product-grid.settings.quick_add.info",
"options": [
{
"value": "none",
"label": "t:sections.main-collection-product-grid.settings.quick_add.options.option_1"
},
{
"value": "standard",
"label": "t:sections.main-collection-product-grid.settings.quick_add.options.option_2"
},
{
"value": "bulk",
"label": "t:sections.main-collection-product-grid.settings.quick_add.options.option_3"
}
]
}
]
},
Create snippet card-product-swatch.liquid
{% if card_product.options_with_values.size > 0 %}
{% for option in card_product.options_with_values %}
{% assign swatch_count = option.values | map: 'swatch' | compact | size %}
{% if swatch_count > 0 %}
<fieldset class="js product-card-swatch product-form__input product-form__input--swatch">
{% if settings.card_product_swatch_show_option_name %}
<legend class="form__label">{{ option.name }}</legend>
{% endif %}
<div style="line-height: 0;">
{% for value in option.values %}
{% if value.swatch %}
{% assign option_position = option.position %}
{% assign option_disabled = true %}
{% assign matched_variant_id = blank %}
{% for variant in card_product.variants %}
{% if option_position == 1 and variant.option1 == value and variant.available %}
{% assign matched_variant_id = variant.id %}
{% assign option_disabled = false %}
{% break %}
{% elsif option_position == 2 and variant.option2 == value and variant.available %}
{% assign matched_variant_id = variant.id %}
{% assign option_disabled = false %}
{% break %}
{% elsif option_position == 3 and variant.option3 == value and variant.available %}
{% assign matched_variant_id = variant.id %}
{% assign option_disabled = false %}
{% break %}
{% endif %}
{% endfor %}
{% capture input_id %}
card-swatch-{{ option.name | handleize }}-{{ forloop.index0 }}-{{ card_product.id }}
{% endcapture %}
<input
type="radio"
class="swatch-input__input card-swatch{% if option_disabled %} disabled{% endif %}"
id="{{ input_id }}"
name="card-swatch-{{ card_product.id }}-{{ option.name | handleize }}"
{% if option_disabled %} disabled{% endif %}
data-product-handle="{{ card_product.handle }}"
data-variant-id="{{ matched_variant_id }}"
data-option-position="{{ option_position }}"
data-swatch-value="{{ value }}"
>
<label for="{{ input_id }}" class="swatch-input__label">
{% render 'swatch', swatch: value.swatch, shape: settings.card_product_swatch_shape %}
</label>
{% endif %}
{% endfor %}
</div>
</fieldset>
{% endif %}
{% endfor %}
{% endif %}
Edit theme.liquid
Load js and css for product card swatches
{% if settings.card_product_swatch_enable %}
{{ 'component-product-variant-picker.css' | asset_url | stylesheet_tag }}
{{ 'component-swatch-input.css' | asset_url | stylesheet_tag }}
{{ 'component-swatch.css' | asset_url | stylesheet_tag }}
<script src="{{ 'card-product-fetch.js' | asset_url }}" defer="defer"></script>
{% style %}
.product-card-swatch.product-form__input--swatch {
position: relative;
z-index: 2;
{% if settings.card_product_swatch_centered %}justify-content: center;{% endif %}
}
.product-card-swatch.product-form__input--swatch .swatch-input__label {
position: relative;
z-index: 2;
}
.product-card-swatch.product-form__input {
flex: 0 0 100%;
padding: 0;
max-width: 44rem;
min-width: fit-content;
border: none;
}
.product-card-swatch.product-form__input--swatch .swatch-input__input+.swatch-input__label {
--swatch-input--size: {{ settings.card_product_swatch_size | default: 2 }}rem;
margin-top: {{ settings.card_product_swatch_margin_top }}rem;
margin-right: {{ settings.card_product_swatch_margin_right }}rem;
margin-bottom: {{ settings.card_product_swatch_margin_bottom }}rem;
margin-left: {{ settings.card_product_swatch_margin_left }}rem;
}
.product-card-swatch .swatch-input__input+.swatch-input__label {
{% if settings.card_product_swatch_shape == 'circle' %}
--swatch-input--border-radius: 50%;
{% else %}
--swatch-input--border-radius: .2rem;
{% endif %}
}
.card__information.card__information--swatch {
padding-top: {{ settings.card_product_information_padding_top }}rem;
padding-bottom: {{ settings.card_product_information_padding_bottom }}rem;
}
{% endstyle %}
{% endif %}
Edit price.liquid
Add target definition
elsif settings.card_product_swatch_enable and card_product_user_selected
assign target = product.selected_or_first_available_variant
Add from pricing text option
elsif settings.card_product_swatch_enable and settings.card_product_swatch_show_from_price and card_product_user_selected and product.price_varies
assign money_price = 'products.product.price.from_price_html' | t: price: money_price
Create section card-product-section.liquid
{% assign card_product = product %}
{% render 'card-product',
card_product: card_product,
media_aspect_ratio: settings.card_product_swatch_image_ratio,
image_shape: settings.card_product_swatch_image_shape,
show_secondary_image: settings.card_product_swatch_show_secondary_image,
show_vendor: settings.card_product_swatch_show_vendor,
show_rating: settings.card_product_swatch_show_rating,
lazy_load: true,
skip_styles: false,
quick_add: settings.card_product_swatch_quick_add,
section_id: section.id,
card_product_user_selected: true
%}
{% schema %}
{
"name": "Card Product Ajax",
"tag": "section",
"class": "section",
"presets": [
{
"name": "Card Product Ajax"
}
]
}
{% endschema %}
Create asset card-product-fetch.js
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('change', onCardSwatchChange);
});
function onCardSwatchChange(event) {
if (!event.target.classList.contains('card-swatch')) return;
const productHandle = event.target.dataset.productHandle;
if (!productHandle) return;
const card = event.target.closest('.card-wrapper.product-card-wrapper');
if (!card) return;
const variantId = event.target.dataset.variantId;
if (!variantId) return;
const sectionId = 'card-product-section';
const url = `/products/${productHandle}?variant=${variantId}§ion_id=${sectionId}`;
fetch(url)
.then((res) => res.text())
.then((responseText) => {
const parser = new DOMParser();
const doc = parser.parseFromString(responseText, 'text/html');
const newCardEl = doc.querySelector(`#card-product-${card.dataset.productId}`);
if (newCardEl) card.replaceWith(newCardEl);
})
.catch(() => {});
}
function buildCardFetchUrl(productHandle, optionValues, sectionId) {
const base = `/products/${productHandle}`;
const params = new URLSearchParams();
params.set('section_id', sectionId);
if (optionValues.length > 0) {
params.set('option_values', optionValues.join(','));
}
return `${base}?${params.toString()}`;
}
function getCardOptionValues(card) {
const fieldsets = card.querySelectorAll('.product-form__input--swatch');
let chosen = [];
fieldsets.forEach((fieldset) => {
const checked = fieldset.querySelector('input[type="radio"]:checked');
if (checked) {
const pos = parseInt(checked.dataset.optionPosition, 10);
const val = checked.dataset.swatchValue;
chosen[pos - 1] = val;
}
});
return chosen.filter((val) => val !== undefined);
}
Edit card-product.liquid
Assign variant_media and card_product_url variables
{% if settings.card_product_swatch_enable and card_product_user_selected %}
{% assign variant_media = card_product.selected_or_first_available_variant.featured_media | default: card_product.featured_media %}
{% assign card_product_url = card_product.url | append: '?variant=' | append: card_product.selected_or_first_available_variant.id %}
{% else %}
{% assign variant_media = card_product.featured_media %}
{% assign card_product_url = card_product.url %}
{% endif %}
Add data attributes to card-wrapper div
id="card-product-{{ card_product.id }}"
data-product-id="{{ card_product.id }}"
Update card_product.featured_media to variant_media in if statement
{%- if variant_media -%}
Update card_product.featured_media to variant_media in <img> element
<img
srcset="
{%- if variant_media.width >= 165 -%}{{ variant_media | image_url: width: 165 }} 165w,{%- endif -%}
{%- if variant_media.width >= 360 -%}{{ variant_media | image_url: width: 360 }} 360w,{%- endif -%}
{%- if variant_media.width >= 533 -%}{{ variant_media | image_url: width: 533 }} 533w,{%- endif -%}
{%- if variant_media.width >= 720 -%}{{ variant_media | image_url: width: 720 }} 720w,{%- endif -%}
{%- if variant_media.width >= 940 -%}{{ variant_media | image_url: width: 940 }} 940w,{%- endif -%}
{%- if variant_media.width >= 1066 -%}{{ variant_media | image_url: width: 1066 }} 1066w,{%- endif -%}
{{ variant_media | image_url }} {{ variant_media.width }}w
"
src="{{ variant_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="{{ variant_media.alt | escape }}"
class="motion-reduce"
{% unless lazy_load == false %}
loading="lazy"
{% endunless %}
width="{{ variant_media.width }}"
height="{{ variant_media.height }}"
>
Update card_product.url to card_product_url variable in card__information hyperlink (2 locations)
<a
href="{{ card_product_url }}"
id="StandardCardNoMediaLink-{{ section_id }}-{{ card_product.id }}"
class="full-unstyled-link"
aria-labelledby="StandardCardNoMediaLink-{{ section_id }}-{{ card_product.id }} NoMediaStandardBadge-{{ section_id }}-{{ card_product.id }}"
>
Update card_product.url to card_product_url variable in quick add modal hyperlink
<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_product.id }}"
data-product-url="{{ card_product_url }}"
>
Render swatches in card__content
{% if settings.card_product_swatch_enable %}
{% render 'card-product-swatch', card_product: card_product %}
{% endif %}
Add new swatch class in card__information
{% if settings.card_product_swatch_enable %}card__information--swatch{% endif %}
Add additional parameter card_product_user_selected in the price render
{% render 'price', product: card_product, price_class: '', show_compare_at_price: true, card_product_user_selected: card_product_user_selected %}