LIQUIDshopifyintermediate
Shopify Variant Selector with Images
Dynamic variant selector that updates product images and price in real-time
Faisal Yaqoob
January 9, 2025
#shopify#liquid#variants#product-options#javascript
Code
liquid
1 {% comment %} 2 Product Variant Selector 3 Add this to your product template 4 {% endcomment %} 5
6 <div class="product-variants" data-product='{{ product | json | escape }}'> 7 {% unless product.has_only_default_variant %} 8 <div class="variant-selectors"> 9 {% for option in product.options_with_values %} 10 <div class="variant-option"> 11 <label for="option-{{ forloop.index0 }}"> 12 {{ option.name }} 13 </label> 14
15 <select 16 id="option-{{ forloop.index0 }}" 17 name="option{{ forloop.index }}" 18 class="variant-select" 19 data-option-index="{{ forloop.index0 }}" 20 > 21 {% for value in option.values %} 22 <option 23 value="{{ value | escape }}" 24 {% if option.selected_value == value %}selected{% endif %} 25 > 26 {{ value }} 27 </option> 28 {% endfor %} 29 </select> 30 </div> 31 {% endfor %} 32 </div> 33 {% endunless %} 34
35 {%- assign current_variant = product.selected_or_first_available_variant -%} 36
37 <div class="product-info"> 38 <div class="product-price" data-price> 39 {% if current_variant.compare_at_price > current_variant.price %} 40 <span class="price-compare">{{ current_variant.compare_at_price | money }}</span> 41 <span class="price-sale">{{ current_variant.price | money }}</span> 42 <span class="price-badge">Save {{ current_variant.compare_at_price | minus: current_variant.price | money }}</span> 43 {% else %} 44 <span class="price">{{ current_variant.price | money }}</span> 45 {% endif %} 46 </div> 47
48 <div class="product-availability" data-availability> 49 {% if current_variant.available %} 50 <span class="in-stock">In Stock</span> 51 {% else %} 52 <span class="out-of-stock">Out of Stock</span> 53 {% endif %} 54 </div> 55 </div> 56
57 <input type="hidden" name="id" data-variant-id value="{{ current_variant.id }}"> 58 </div> 59
60 <script> 61 class VariantSelector { 62 constructor(element) { 63 this.element = element; 64 this.productData = JSON.parse(element.dataset.product); 65 this.selects = element.querySelectorAll('.variant-select'); 66 this.priceElement = element.querySelector('[data-price]'); 67 this.availabilityElement = element.querySelector('[data-availability]'); 68 this.variantIdInput = element.querySelector('[data-variant-id]'); 69
70 this.init(); 71 } 72
73 init() { 74 this.selects.forEach(select => { 75 select.addEventListener('change', () => this.onVariantChange()); 76 }); 77 } 78
79 onVariantChange() { 80 const selectedOptions = Array.from(this.selects).map(select => select.value); 81 const variant = this.getVariantFromOptions(selectedOptions); 82
83 if (variant) { 84 this.updatePrice(variant); 85 this.updateAvailability(variant); 86 this.updateImage(variant); 87 this.updateURL(variant); 88 this.variantIdInput.value = variant.id; 89
90 // Dispatch custom event 91 this.element.dispatchEvent(new CustomEvent('variant:changed', { 92 detail: { variant } 93 })); 94 } 95 } 96
97 getVariantFromOptions(options) { 98 return this.productData.variants.find(variant => { 99 return variant.options.every((option, index) => { 100 return options[index] === option; 101 }); 102 }); 103 } 104
105 updatePrice(variant) { 106 let priceHTML = ''; 107
108 if (variant.compare_at_price && variant.compare_at_price > variant.price) { 109 const savings = variant.compare_at_price - variant.price; 110 priceHTML = ` 111 <span class="price-compare">${this.formatMoney(variant.compare_at_price)}</span> 112 <span class="price-sale">${this.formatMoney(variant.price)}</span> 113 <span class="price-badge">Save ${this.formatMoney(savings)}</span> 114 `; 115 } else { 116 priceHTML = `<span class="price">${this.formatMoney(variant.price)}</span>`; 117 } 118
119 this.priceElement.innerHTML = priceHTML; 120 } 121
122 updateAvailability(variant) { 123 const availableHTML = variant.available 124 ? '<span class="in-stock">In Stock</span>' 125 : '<span class="out-of-stock">Out of Stock</span>'; 126
127 this.availabilityElement.innerHTML = availableHTML; 128 } 129
130 updateImage(variant) { 131 if (!variant.featured_image) return; 132
133 const productImage = document.querySelector('.product-image img'); 134 if (productImage) { 135 productImage.src = variant.featured_image.src; 136 productImage.alt = variant.featured_image.alt || variant.name; 137 } 138 } 139
140 updateURL(variant) { 141 if (!window.history.replaceState) return; 142
143 const url = new URL(window.location.href); 144 url.searchParams.set('variant', variant.id); 145 window.history.replaceState({}, '', url.toString()); 146 } 147
148 formatMoney(cents) { 149 return new Intl.NumberFormat('en-US', { 150 style: 'currency', 151 currency: '{{ shop.currency }}' 152 }).format(cents / 100); 153 } 154 } 155
156 // Initialize 157 document.addEventListener('DOMContentLoaded', () => { 158 const variantSelector = document.querySelector('.product-variants'); 159 if (variantSelector) { 160 new VariantSelector(variantSelector); 161 } 162 }); 163 </script>
Shopify Variant Selector with Images
A dynamic variant selector that updates product images, price, and availability when customers select different options.
{% comment %}
Product Variant Selector
Add this to your product template
{% endcomment %}
<div class="product-variants" data-product='{{ product | json | escape }}'>
{% unless product.has_only_default_variant %}
<div class="variant-selectors">
{% for option in product.options_with_values %}
<div class="variant-option">
<label for="option-{{ forloop.index0 }}">
{{ option.name }}
</label>
<select
id="option-{{ forloop.index0 }}"
name="option{{ forloop.index }}"
class="variant-select"
data-option-index="{{ forloop.index0 }}"
>
{% for value in option.values %}
<option
value="{{ value | escape }}"
{% if option.selected_value == value %}selected{% endif %}
>
{{ value }}
</option>
{% endfor %}
</select>
</div>
{% endfor %}
</div>
{% endunless %}
{%- assign current_variant = product.selected_or_first_available_variant -%}
<div class="product-info">
<div class="product-price" data-price>
{% if current_variant.compare_at_price > current_variant.price %}
<span class="price-compare">{{ current_variant.compare_at_price | money }}</span>
<span class="price-sale">{{ current_variant.price | money }}</span>
<span class="price-badge">Save {{ current_variant.compare_at_price | minus: current_variant.price | money }}</span>
{% else %}
<span class="price">{{ current_variant.price | money }}</span>
{% endif %}
</div>
<div class="product-availability" data-availability>
{% if current_variant.available %}
<span class="in-stock">In Stock</span>
{% else %}
<span class="out-of-stock">Out of Stock</span>
{% endif %}
</div>
</div>
<input type="hidden" name="id" data-variant-id value="{{ current_variant.id }}">
</div>
<script>
class VariantSelector {
constructor(element) {
this.element = element;
this.productData = JSON.parse(element.dataset.product);
this.selects = element.querySelectorAll('.variant-select');
this.priceElement = element.querySelector('[data-price]');
this.availabilityElement = element.querySelector('[data-availability]');
this.variantIdInput = element.querySelector('[data-variant-id]');
this.init();
}
init() {
this.selects.forEach(select => {
select.addEventListener('change', () => this.onVariantChange());
});
}
onVariantChange() {
const selectedOptions = Array.from(this.selects).map(select => select.value);
const variant = this.getVariantFromOptions(selectedOptions);
if (variant) {
this.updatePrice(variant);
this.updateAvailability(variant);
this.updateImage(variant);
this.updateURL(variant);
this.variantIdInput.value = variant.id;
// Dispatch custom event
this.element.dispatchEvent(new CustomEvent('variant:changed', {
detail: { variant }
}));
}
}
getVariantFromOptions(options) {
return this.productData.variants.find(variant => {
return variant.options.every((option, index) => {
return options[index] === option;
});
});
}
updatePrice(variant) {
let priceHTML = '';
if (variant.compare_at_price && variant.compare_at_price > variant.price) {
const savings = variant.compare_at_price - variant.price;
priceHTML = `
<span class="price-compare">${this.formatMoney(variant.compare_at_price)}</span>
<span class="price-sale">${this.formatMoney(variant.price)}</span>
<span class="price-badge">Save ${this.formatMoney(savings)}</span>
`;
} else {
priceHTML = `<span class="price">${this.formatMoney(variant.price)}</span>`;
}
this.priceElement.innerHTML = priceHTML;
}
updateAvailability(variant) {
const availableHTML = variant.available
? '<span class="in-stock">In Stock</span>'
: '<span class="out-of-stock">Out of Stock</span>';
this.availabilityElement.innerHTML = availableHTML;
}
updateImage(variant) {
if (!variant.featured_image) return;
const productImage = document.querySelector('.product-image img');
if (productImage) {
productImage.src = variant.featured_image.src;
productImage.alt = variant.featured_image.alt || variant.name;
}
}
updateURL(variant) {
if (!window.history.replaceState) return;
const url = new URL(window.location.href);
url.searchParams.set('variant', variant.id);
window.history.replaceState({}, '', url.toString());
}
formatMoney(cents) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: '{{ shop.currency }}'
}).format(cents / 100);
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
const variantSelector = document.querySelector('.product-variants');
if (variantSelector) {
new VariantSelector(variantSelector);
}
});
</script>
Styling
.variant-selectors {
margin-bottom: 20px;
}
.variant-option {
margin-bottom: 15px;
}
.variant-option label {
display: block;
font-weight: 600;
margin-bottom: 8px;
font-size: 14px;
}
.variant-select {
width: 100%;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
background: white;
cursor: pointer;
transition: border-color 0.3s;
}
.variant-select:hover {
border-color: #999;
}
.variant-select:focus {
outline: none;
border-color: #000;
}
.product-price {
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
font-size: 24px;
font-weight: 700;
}
.price-compare {
text-decoration: line-through;
color: #999;
font-size: 20px;
}
.price-sale {
color: #e53935;
}
.price-badge {
background: #e53935;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
font-weight: 600;
}
.product-availability {
margin-bottom: 20px;
}
.in-stock {
color: #10b981;
font-weight: 600;
}
.out-of-stock {
color: #ef4444;
font-weight: 600;
}
Features
- Real-time Updates: Price, availability, and images update instantly
- URL Updates: Variant changes update the URL for sharing
- Custom Events: Dispatches events for integration with other scripts
- Progressive Enhancement: Works with or without JavaScript
- Mobile Friendly: Responsive design with touch-friendly selectors
Alternative: Swatch Variant Selector
For color/image swatches instead of dropdowns:
<div class="variant-swatches">
{% for option in product.options_with_values %}
{% if option.name == 'Color' %}
<div class="swatch-group">
<label>{{ option.name }}</label>
<div class="swatches">
{% for value in option.values %}
<button
type="button"
class="swatch {% if option.selected_value == value %}active{% endif %}"
data-value="{{ value | escape }}"
>
{{ value }}
</button>
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
Related Snippets
Shopify Product Recommendations
Display related products using Shopify's built-in recommendation engine
LIQUIDshopifybeginner
liquidPreview
{% comment %}
Product Recommendations Section
Add this to your product template or create as a section
{% endcomment %}
...#shopify#liquid#recommendations+2
1/9/2025
View
Shopify AJAX Add to Cart
Add products to cart without page reload using Shopify AJAX API
JAVASCRIPTshopifyintermediate
javascriptPreview
// Add to cart with AJAX
function addToCart(variantId, quantity = 1) {
const formData = {
items: [{
...#shopify#ajax#cart+2
1/9/2025
View