All things Shopify and commerce
Hi everyone, I hope you're doing well!
I'm using the Dawn theme on Shopify and I would like to add image swatches for product variants (such as colour options), without using any external apps, to keep my store lightweight. I've searched for working code and tutorials, but I haven’t found anything clear or helpful. I’m looking for a working code snippet and precise instructions on where exactly to place it within Shopify’s code editor.
Can anyone help me with this or point me in the right direction? I’d really appreciate it!
Hello @vhboy To add image swatches for color variants in the Dawn theme on Shopify (without using apps), you’ll need to customize the main-product.liquid, product-variant-picker.liquid, and global.js files. I’ll walk you through the full process to achieve this cleanly.
What You'll Achieve:
. Replace the default color variant dropdown or pills with clickable image swatches.
. Keep it lightweight, without apps.
. Show selected state clearly.
Prerequisites:
. You're using Dawn 9.0.0+ (let me know if your version is older).
. Your product variants have images assigned per color.
Step-by-Step Instructions:
1. Assign Images to Each Variant
In your Shopify Admin:
. Go to Products > [Your Product].
. Scroll to Variants, click each one, and upload an image that represents the color.
2. Edit main-product.liquid
File path: Sections/main-product.liquid
Look for this block (or similar):
{% render 'product-variant-picker', ... %}
It should already be rendering the picker, so no changes needed here unless it’s missing.
3.
Modify product-variant-picker.liquid
File path: Snippets/product-variant-picker.liquid
Replace the contents only for the color option with image swatches.
Find this loop:
{% for option in product.options_with_values %}
And inside it, look for:
{% if option.name == 'Color' %}
If it doesn’t exist, wrap it yourself like this:
Replace or Insert this inside the loop:
{% if option.name == 'Color' %}
<fieldset class="variant-fieldset">
<legend class="form__label">{{ option.name }}</legend>
<div class="swatch-container">
{% for value in option.values %}
{% assign downcased_value = value | downcase | replace: ' ', '-' %}
{% assign variant_image = null %}
{% for variant in product.variants %}
{% if variant.options contains value %}
{% assign variant_image = variant.image %}
{% break %}
{% endif %}
{% endfor %}
<label class="swatch-label">
<input type="radio"
name="option-{{ option.position }}"
value="{{ value }}"
form="{{ product_form_id }}"
{% if option.selected_value == value %}checked{% endif %}
>
{% if variant_image %}
<img src="{{ variant_image | image_url: width: 50 }}" alt="{{ value }}" class="swatch-image">
{% else %}
<span class="swatch-fallback">{{ value }}</span>
{% endif %}
</label>
{% endfor %}
</div>
</fieldset>
{% else %}
{% render 'product-variant-options', product: product, option: option, product_form_id: product_form_id %}
{% endif %}
This creates image swatches instead of default text/pills for Color.
4. Add CSS for Swatches
File path: Assets/base.css or Assets/component-product.css
Scroll to the bottom and add this:
.swatch-container {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin: 1rem 0;
}
.swatch-label {
position: relative;
cursor: pointer;
border: 2px solid transparent;
padding: 2px;
border-radius: 4px;
}
.swatch-label input[type="radio"] {
display: none;
}
.swatch-label .swatch-image {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 4px;
border: 1px solid #ccc;
}
.swatch-label input[type="radio"]:checked + .swatch-image {
border: 2px solid #000;
}
Done! Test It Out:
1. Visit a product page with color variants.
2. Swatches should appear.
3. Selecting a swatch should change the product image and update the variant selection.
Would you like me to help add hover titles, color names under images, or support for mobile styling
plz let me know
Thank you 😊
Hello @goldi07 , hope you're doing well. Thank you for helping me out.
I have a few questions, if you don’t mind clearing them up for me:
I couldn’t find where to place the code in product-variant-picker.liquid. I don’t really understand much about loops and things like that — would it be okay if I send you my full code so you can add it for me? Or perhaps you could tell me exactly which line to insert the code on, if that’s not too much trouble.
{% comment %}
Renders product variant-picker
Accepts:
- product: {Object} product object.
- block: {Object} passing the block information.
- product_form_id: {String} Id of the product form to which the variant picker is associated.
Usage:
{% render 'product-variant-picker', product: product, block: block, product_form_id: product_form_id %}
{% endcomment %}
{%- unless product.has_only_default_variant -%}
<variant-selects
id="variant-selects-{{ section.id }}"
data-section="{{ section.id }}"
{{ block.shopify_attributes }}
>
{%- for option in product.options_with_values -%}
{%- liquid
assign swatch_count = option.values | map: 'swatch' | compact | size
assign picker_type = block.settings.picker_type
if swatch_count > 0 and block.settings.swatch_shape != 'none'
if block.settings.picker_type == 'dropdown'
assign picker_type = 'swatch_dropdown'
else
assign picker_type = 'swatch'
endif
endif
-%}
{%- if picker_type == 'swatch' -%}
<fieldset class="js product-form__input product-form__input--swatch">
<legend class="form__label">
{{ option.name }}:
<span data-selected-value>
{{- option.selected_value -}}
</span>
</legend>
{% render 'product-variant-options',
product: product,
option: option,
block: block,
picker_type: picker_type
%}
</fieldset>
{%- elsif picker_type == 'button' -%}
<fieldset class="js product-form__input product-form__input--pill">
<legend class="form__label">{{ option.name }}</legend>
{% render 'product-variant-options',
product: product,
option: option,
block: block,
picker_type: picker_type
%}
</fieldset>
{%- else -%}
<div class="product-form__input product-form__input--dropdown">
<label class="form__label" for="Option-{{ section.id }}-{{ forloop.index0 }}">
{{ option.name }}
</label>
<div class="select">
{%- if picker_type == 'swatch_dropdown' -%}
<span
data-selected-value
class="dropdown-swatch"
>
{% render 'swatch', swatch: option.selected_value.swatch, shape: block.settings.swatch_shape %}
</span>
{%- endif -%}
<select
id="Option-{{ section.id }}-{{ forloop.index0 }}"
class="select__select"
name="options[{{ option.name | escape }}]"
form="{{ product_form_id }}"
>
{% render 'product-variant-options',
product: product,
option: option,
block: block,
picker_type: picker_type
%}
</select>
<span class="svg-wrapper">
{{- 'icon-caret.svg' | inline_asset_content -}}
</span>
</div>
</div>
{%- endif -%}
{%- endfor -%}
<script type="application/json" data-selected-variant>
{{ product.selected_or_first_available_variant | json }}
</script>
</variant-selects>
{%- endunless -%}
@
yes, it helps a lot to see your actual product-variant-picker.liquid snippet. I’ll guide you through an exact modification so you can add image swatches for the Color option only, while keeping the other options as they are (dropdowns, buttons, etc.).
Where to Insert the Code
We’ll be modifying just one part inside your existing loop:
{%- for option in product.options_with_values -%}
That loop is already rendering different picker types (swatch, button, dropdown) based on picker_type.
We’ll now intercept that flow for just the Color option and render our custom image swatch block instead.
Step-by-Step Code Insert
1. Inside your loop, just after this line:
Add this conditional right after the loop starts:
{%- if option.name == 'Color' -%}
<fieldset class="js product-form__input product-form__input--swatch">
<legend class="form__label">
{{ option.name }}:
<span data-selected-value>
{{- option.selected_value -}}
</span>
</legend>
<div class="swatch-container">
{%- for value in option.values -%}
{%- assign downcased_value = value | downcase | replace: ' ', '-' -%}
{%- assign variant_image = null -%}
{%- for variant in product.variants -%}
{%- if variant.options contains value -%}
{%- assign variant_image = variant.image -%}
{%- break -%}
{%- endif -%}
{%- endfor -%}
<label class="swatch-label">
<input
type="radio"
name="options[{{ option.name }}]"
value="{{ value }}"
form="{{ product_form_id }}"
{% if option.selected_value == value %}checked{% endif %}
>
{% if variant_image %}
<img
src="{{ variant_image | image_url: width: 60 }}"
alt="{{ value }}"
class="swatch-image"
>
{% else %}
<span class="swatch-fallback">{{ value }}</span>
{% endif %}
</label>
{%- endfor -%}
</div>
</fieldset>
{%- else -%}
3. Then, scroll down and find this line (which closes your current picker rendering):
{%- endif -%}
4. Just after that, add:
{%- endif -%}
This closes your custom if option.name == 'Color' logic cleanly.
Your Updated Flow Will Look Like:
{%- for option in product.options_with_values -%}
{%- if option.name == 'Color' -%}
<!-- Custom image swatch fieldset here -->
{%- else -%}
<!-- Your existing picker logic for other options -->
{%- if picker_type == 'swatch' -%}
...
{%- elsif picker_type == 'button' -%}
...
{%- else -%}
...
{%- endif -%}
{%- endif -%}
{%- endfor -%}
CSS Reminder
In your Assets/base.css (or component-product.css), don’t forget to add:
.swatch-container {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.swatch-label {
position: relative;
cursor: pointer;
border: 2px solid transparent;
padding: 2px;
border-radius: 4px;
}
.swatch-label input[type="radio"] {
display: none;
}
.swatch-image {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 4px;
border: 1px solid #ccc;
}
.swatch-label input[type="radio"]:checked + .swatch-image {
border: 2px solid #000;
}
try this and let me if this work i also add a sacreenshot
thank you 😊
I managed to add it successfully, but when I click on the variant, which is now an image, it doesn't switch. It's only possible to view the colour without being able to change it. Do you know how to fix this? I’ll send you my code so you can see how it turned out.
{% comment %}
Renders product variant-picker
Accepts:
- product: {Object} product object.
- block: {Object} passing the block information.
- product_form_id: {String} Id of the product form to which the variant picker is associated.
Usage:
{% render 'product-variant-picker', product: product, block: block, product_form_id: product_form_id %}
{% endcomment %}
{%- unless product.has_only_default_variant -%}
<variant-selects
id="variant-selects-{{ section.id }}"
data-section="{{ section.id }}"
{{ block.shopify_attributes }}
>
{%- for option in product.options_with_values -%}
{%- if option.name == 'Color' -%}
<fieldset class="js product-form__input product-form__input--swatch">
<legend class="form__label">
{{ option.name }}:
<span data-selected-value>
{{- option.selected_value -}}
</span>
</legend>
<div class="swatch-container">
{%- for value in option.values -%}
{%- assign downcased_value = value | downcase | replace: ' ', '-' -%}
{%- assign variant_image = null -%}
{%- for variant in product.variants -%}
{%- if variant.options contains value -%}
{%- assign variant_image = variant.image -%}
{%- break -%}
{%- endif -%}
{%- endfor -%}
<label class="swatch-label">
<input
type="radio"
name="options[{{ option.name }}]"
value="{{ value }}"
form="{{ product_form_id }}"
{% if option.selected_value == value %}checked{% endif %}
>
{% if variant_image %}
<img
src="{{ variant_image | image_url: width: 60 }}"
alt="{{ value }}"
class="swatch-image"
>
{% else %}
<span class="swatch-fallback">{{ value }}</span>
{% endif %}
</label>
{%- endfor -%}
</div>
</fieldset>
{%- else -%}
{%- liquid
assign swatch_count = option.values | map: 'swatch' | compact | size
assign picker_type = block.settings.picker_type
if swatch_count > 0 and block.settings.swatch_shape != 'none'
if block.settings.picker_type == 'dropdown'
assign picker_type = 'swatch_dropdown'
else
assign picker_type = 'swatch'
endif
endif
-%}
{%- if picker_type == 'swatch' -%}
<fieldset class="js product-form__input product-form__input--swatch">
<legend class="form__label">
{{ option.name }}:
<span data-selected-value>
{{- option.selected_value -}}
</span>
</legend>
{% render 'product-variant-options',
product: product,
option: option,
block: block,
picker_type: picker_type
%}
</fieldset>
{%- elsif picker_type == 'button' -%}
<fieldset class="js product-form__input product-form__input--pill">
<legend class="form__label">{{ option.name }}</legend>
{% render 'product-variant-options',
product: product,
option: option,
block: block,
picker_type: picker_type
%}
</fieldset>
{%- else -%}
<div class="product-form__input product-form__input--dropdown">
<label class="form__label" for="Option-{{ section.id }}-{{ forloop.index0 }}">
{{ option.name }}
</label>
<div class="select">
{%- if picker_type == 'swatch_dropdown' -%}
<span
data-selected-value
class="dropdown-swatch"
>
{% render 'swatch', swatch: option.selected_value.swatch, shape: block.settings.swatch_shape %}
</span>
{%- endif -%}
<select
id="Option-{{ section.id }}-{{ forloop.index0 }}"
class="select__select"
name="options[{{ option.name | escape }}]"
form="{{ product_form_id }}"
>
{% render 'product-variant-options',
product: product,
option: option,
block: block,
picker_type: picker_type
%}
</select>
<span class="svg-wrapper">
{{- 'icon-caret.svg' | inline_asset_content -}}
</span>
</div>
</div>
{%- endif -%}
{%- endif -%}
{%- endfor -%}
<script type="application/json" data-selected-variant>
{{ product.selected_or_first_available_variant | json }}
</script>
</variant-selects>
{%- endunless -%}
hey @vhboy Thanks for sharing your code, — you're super close! The issue is that while your custom image swatches are rendering perfectly, they’re missing the JavaScript trigger that tells Shopify’s <variant-selects> component to update the selected variant when you change the radio button.
Here’s what’s happening:
. The <input type="radio"> is being rendered correctly.
. But Shopify’s built-in variant-selects.js component listens for change events on the <select> and default <input> elements it renders.
. Since we’ve custom-rendered the radio inputs ourselves, we need to dispatch a change event manually for them to work with Dawn’s logic.
Solution: Add JS Event Trigger for Your Custom Radio Inputs
Step 1: Add a small JS block below your </variant-selects> (right after the </variant-selects> tag)
Add this just after:
</variant-selects>
<script>
document.querySelectorAll('[name^="options[Color]"]').forEach((input) => {
input.addEventListener('change', function () {
// Trigger native change event for Shopify's variant-selects
const variantSelectsEl = this.closest('variant-selects');
if (variantSelectsEl) {
variantSelectsEl.dispatchEvent(new Event('change', { bubbles: true }));
}
});
});
</script>
Bonus: Fix Image Click Target (Optional UX)
Make your <img> inside the label clickable by adding a for and id link:
Update your input and image area like this:
<input
type="radio"
id="swatch-{{ option.name | handle }}-{{ value | handle }}"
name="options[{{ option.name }}]"
value="{{ value }}"
form="{{ product_form_id }}"
{% if option.selected_value == value %}checked{% endif %}
/>
<label class="swatch-label" for="swatch-{{ option.name | handle }}-{{ value | handle }}">
{% if variant_image %}
<img
src="{{ variant_image | image_url: width: 60 }}"
alt="{{ value }}"
class="swatch-image"
>
{% else %}
<span class="swatch-fallback">{{ value }}</span>
{% endif %}
</label>
So, instead of nesting the <input> inside the <label>, you explicitly pair them via id and for. Shopify's default styles and behavior work best with this structure.
Once you add the JavaScript snippet and update the label structure if you want, the swatches will trigger the correct variant selection, and the product media / price / availability will update accordingly — just like with native controls.
if you need more help plz let me know
Thank you 😊
Hi my friend, how are you? I hope you're having an excellent day. First of all, I want to thank you for your patience and help—it's really been a huge support!
I managed to do everything you mentioned, but when I click on the image, it still doesn't change the variant. I'd like to know what's missing or what I might have done wrong. I'll leave my code below.
{% comment %}
Renders product variant-picker
Accepts:
- product: {Object} product object.
- block: {Object} passing the block information.
- product_form_id: {String} Id of the product form to which the variant picker is associated.
Usage:
{% render 'product-variant-picker', product: product, block: block, product_form_id: product_form_id %}
{% endcomment %}
{%- unless product.has_only_default_variant -%}
<variant-selects
id="variant-selects-{{ section.id }}"
data-section="{{ section.id }}"
{{ block.shopify_attributes }}
>
{%- for option in product.options_with_values -%}
{%- if option.name == 'Color' -%}
<fieldset class="js product-form__input product-form__input--swatch">
<legend class="form__label">
{{ option.name }}:
<span data-selected-value>
{{- option.selected_value -}}
</span>
</legend>
<div class="swatch-container">
{%- for value in option.values -%}
{%- assign downcased_value = value | downcase | replace: ' ', '-' -%}
{%- assign variant_image = null -%}
{%- for variant in product.variants -%}
{%- if variant.options contains value -%}
{%- assign variant_image = variant.image -%}
{%- break -%}
{%- endif -%}
{%- endfor -%}
<label class="swatch-label">
<input
type="radio"
id="swatch-{{ option.name | handle }}-{{ value | handle }}"
name="options[{{ option.name }}]"
value="{{ value }}"
form="{{ product_form_id }}"
{% if option.selected_value == value %}checked{% endif %}}
/>
<label class="swatch-label" for="swatch-{{ option.name | handle }}-{{ value | handle }}">
{% if variant_image %}
<img
src="{{ variant_image | image_url: width: 60 }}"
alt="{{ value }}"
class="swatch-image"
>
{% else %}
<span class="swatch-fallback">{{ value }}</span>
{% endif %}
</label>
{%- endfor -%}
</div>
</fieldset>
{%- else -%}
{%- liquid
assign swatch_count = option.values | map: 'swatch' | compact | size
assign picker_type = block.settings.picker_type
if swatch_count > 0 and block.settings.swatch_shape != 'none'
if block.settings.picker_type == 'dropdown'
assign picker_type = 'swatch_dropdown'
else
assign picker_type = 'swatch'
endif
endif
-%}
{%- if picker_type == 'swatch' -%}
<fieldset class="js product-form__input product-form__input--swatch">
<legend class="form__label">
{{ option.name }}:
<span data-selected-value>
{{- option.selected_value -}}
</span>
</legend>
{% render 'product-variant-options',
product: product,
option: option,
block: block,
picker_type: picker_type
%}
</fieldset>
{%- elsif picker_type == 'button' -%}
<fieldset class="js product-form__input product-form__input--pill">
<legend class="form__label">{{ option.name }}</legend>
{% render 'product-variant-options',
product: product,
option: option,
block: block,
picker_type: picker_type
%}
</fieldset>
{%- else -%}
<div class="product-form__input product-form__input--dropdown">
<label class="form__label" for="Option-{{ section.id }}-{{ forloop.index0 }}">
{{ option.name }}
</label>
<div class="select">
{%- if picker_type == 'swatch_dropdown' -%}
<span
data-selected-value
class="dropdown-swatch"
>
{% render 'swatch', swatch: option.selected_value.swatch, shape: block.settings.swatch_shape %}
</span>
{%- endif -%}
<select
id="Option-{{ section.id }}-{{ forloop.index0 }}"
class="select__select"
name="options[{{ option.name | escape }}]"
form="{{ product_form_id }}"
>
{% render 'product-variant-options',
product: product,
option: option,
block: block,
picker_type: picker_type
%}
</select>
<span class="svg-wrapper">
{{- 'icon-caret.svg' | inline_asset_content -}}
</span>
</div>
</div>
{%- endif -%}
{%- endif -%}
{%- endfor -%}
<script type="application/json" data-selected-variant>
{{ product.selected_or_first_available_variant | json }}
</script>
</variant-selects>
<script>
document.querySelectorAll('[name^="options[Color]"]').forEach((input) => {
input.addEventListener('change', function () {
// Trigger native change event for Shopify's variant-selects
const variantSelectsEl = this.closest('variant-selects');
if (variantSelectsEl) {
variantSelectsEl.dispatchEvent(new Event('change', { bubbles: true }));
}
});
});
</script>
{%- endunless -%}
I can't do what this video suggests with existing products — I'd have to register everything from scratch, and that wasn't what I was looking for. But I really appreciate your help, mate. @gr_trading
Hi @vhboy ,
Please refer the below video to see how you can activate color swatch in DAWN theme.
Learn how to build powerful custom workflows in Shopify Flow with expert guidance from ...
By Jacqui May 7, 2025Did You Know? May is named after Maia, the Roman goddess of growth and flourishing! ...
By JasonH May 2, 2025Discover opportunities to improve SEO with new guidance available from Shopify’s growth...
By Jacqui May 1, 2025