","programmingLanguage":"LIQUID","codeRepository":"https://fysalyaqoob.com/snippets/shopify-variant-selector","url":"https://fysalyaqoob.com/snippets/shopify-variant-selector","datePublished":"2025-01-09","dateModified":"2025-01-09","author":{"@type":"Person","name":"Faisal Yaqoob","url":"https://fysalyaqoob.com","jobTitle":"Senior WordPress & Shopify Developer"},"keywords":"shopify, liquid, variants, product-options, javascript","about":{"@type":"Thing","name":"shopify","description":"shopify development"},"educationalLevel":"intermediate","isAccessibleForFree":true,"license":"https://opensource.org/licenses/MIT"}
LIQUIDshopifyintermediate

Shopify Variant Selector with Images

Dynamic variant selector that updates product images and price in real-time

#shopify#liquid#variants#product-options#javascript
Share this snippet:

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>
61class 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
157document.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