Skip to content
Optimizing Your Variant Combinations For Better Conversions - Free Tutorial
Browse other ways to boost conversion rate & profit

Optimizing Your Variant Combinations For Better Conversions - Free Tutorial

In this tutorial, we're looking at an effective way to tackle choice paralysis on your Shopify store by removing variant combinations from your storefront—without altering any backend products. Imagine landing on a product page with endless options and feeling overwhelmed; it happens often in ecommerce and can result in lost sales. Reducing visible options helps improve your conversion rate and even addresses Shopify’s 100 variant limit.

Compatible Themes: This code should work on all free Shopify themes (Dawn, Refresh, Craft, Studio, Publisher, Crave, Origin, Taste, Colorblock, Sense, Ride, Spotlight).

 

Edit main-product.liquid schema

Add settings to the variant_picker block

        {
          "type": "header",
          "content": "Invalid Variants"
        },
        {
          "type": "checkbox",
          "id": "hide_invalid_variants",
          "label": "Hide invalid variants",
          "default": false,
          "info": "When enabled, invalid variant combinations will be hidden from the variant picker."
        }

Edit product-variant-picker.liquid

Add data-hide-invalid-variants attribute to the <variant-selects> tag

data-hide-invalid-variants="{{ block.settings.hide_invalid_variants }}"

Add data attribute script at bottom before closing of </variant-selects>

<script type="application/json" id="data-variant-validity-{{ section.id  }}">
  {
    {% for option1_value in product.options_with_values[0].values %}
      {% assign option1 = product.options_with_values[0].name %}
      "{{ option1_value | handleize }}": {
        {% if product.options.size > 1 %}
          {% for option2_value in product.options_with_values[1].values %}
            {% assign option2 = product.options_with_values[1].name %}
            "{{ option2_value | handleize }}": {
              {% if product.options.size > 2 %}
                {% for option3_value in product.options_with_values[2].values %}
                  {% assign option3 = product.options_with_values[2].name %}
                  {% assign variant_exists = false %}
                  {% for variant in product.variants %}
                    {% if variant.option1 == option1_value and variant.option2 == option2_value and variant.option3 == option3_value %}
                      {% assign variant_exists = true %}
                      {% break %}
                    {% endif %}
                  {% endfor %}
                  "{{ option3_value | handleize }}": {{ variant_exists | json }}{% unless forloop.last %},{% endunless %}
                {% endfor %}
              {% else %}
                {% assign variant_exists = false %}
                {% for variant in product.variants %}
                  {% if variant.option1 == option1_value and variant.option2 == option2_value %}
                    {% assign variant_exists = true %}
                    {% break %}
                  {% endif %}
                {% endfor %}
                "valid": {{ variant_exists | json }}
              {% endif %}
            }{% unless forloop.last %},{% endunless %}
          {% endfor %}
        {% else %}
          {% assign variant_exists = false %}
          {% for variant in product.variants %}
            {% if variant.option1 == option1_value %}
              {% assign variant_exists = true %}
              {% break %}
            {% endif %}
          {% endfor %}
          "valid": {{ variant_exists | json }}
        {% endif %}
      }{% unless forloop.last %},{% endunless %}
    {% endfor %}
  }
</script>

Edit component-product-variant-picker.css

Add additional class to bottom of the file

.variant-select__hidden {
  display: none;
}

Edit global.js

Update VariantSelects class in global.js with the following version

class VariantSelects extends HTMLElement {

  constructor() {
    super();
    this.isCustomSelecting = false;
    this.variantValidityData = JSON.parse(document.getElementById(`data-variant-validity-${this.dataset.section}`).textContent);
    this.hideInvalidVariants = this.dataset.hideInvalidVariants === 'true';
  }
  
  connectedCallback() {
    this.addEventListener('change', (event) => {
        if (!this.isCustomSelecting && this.hideInvalidVariants) {
          const selectedVariant = this.getSelectedVariant();
          if (!this.isValidVariant(selectedVariant)) {  
            event.preventDefault();
            this.selectCustomVariant(event.target);
          } else {
            this.handlePropagation(event);
          }
        } else {
          this.handlePropagation(event);
        }      
    });
    if (this.hideInvalidVariants) {
      this.hideInvalidVariantOptions();
    }
  }
  
  handlePropagation(event) {
    const target = this.getInputForEventTarget(event.target);
    this.updateSelectionMetadata(event);
    publish(PUB_SUB_EVENTS.optionValueSelectionChange, {
      data: {
        event,
        target,
        selectedOptionValues: this.selectedOptionValues,
      },
    });
  }
  
  isValidVariant(selectedVariant) {
    let validityCheck = this.variantValidityData;
    for (let option of selectedVariant) {
      option = this.transformOptionValue(option);
      validityCheck = validityCheck[option];
      if (!validityCheck) return false;
    }
    return validityCheck.hasOwnProperty('valid') ? validityCheck.valid : true;
  }

  transformOptionValue(option) {
    return option.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+$/, '');
  }
  
  getSelectedVariant() {
    return Array.from(this.querySelectorAll('fieldset')).map(
      fieldset => fieldset.querySelector('input:checked').value
    );
  }

  selectCustomVariant(originalTarget) {
    this.isCustomSelecting = true;
    const allFieldsets = Array.from(this.querySelectorAll('fieldset'));
    let changedOption = false;
    for (let i = allFieldsets.length - 1; i >= 0; i--) {
      const fieldset = allFieldsets[i];
      const options = Array.from(fieldset.querySelectorAll('input[type="radio"]'));
      if (options.length > 0) {
        let currentIndex = options.findIndex(option => option.checked);
        let nextValidOption = this.findNextValidOption(options, currentIndex, i);
        if (nextValidOption) {
          nextValidOption.checked = true;
          const newEvent = new Event('change', { bubbles: true });
          nextValidOption.dispatchEvent(newEvent);
          changedOption = true;
          break;
        }
      }
    }
    if (!changedOption) {
      this.resetToFirstValidCombination();
    }
    this.isCustomSelecting = false;
  }
  
  resetToFirstValidCombination() {
    const allFieldsets = Array.from(this.querySelectorAll('fieldset'));
    for (let i = 0; i < allFieldsets.length; i++) {
      const fieldset = allFieldsets[i];
      const options = Array.from(fieldset.querySelectorAll('input[type="radio"]'));
      for (let option of options) {
        option.checked = true;
        if (this.isValidVariant(this.getSelectedVariant())) {
          const newEvent = new Event('change', { bubbles: true });
          option.dispatchEvent(newEvent);
          return;
        }
      }
    }
  }
  
  findNextValidOption(options, currentIndex, optionLevel) {
    const allFieldsets = Array.from(this.querySelectorAll('fieldset'));
    const selectedOptions = allFieldsets.map(
      fieldset => this.transformOptionValue(fieldset.querySelector('input:checked').value)
    );
    const findValidCombination = (currentOptions, level) => {
      if (level === allFieldsets.length) {
        return this.isValidVariant(currentOptions) ? currentOptions : null;
      }
      const fieldset = allFieldsets[level];
      const fieldOptions = Array.from(fieldset.querySelectorAll('input[type="radio"]'));
      for (let option of fieldOptions) {
        const optionValue = this.transformOptionValue(option.value);
        const newOptions = [...currentOptions];
        newOptions[level] = optionValue;
        const result = findValidCombination(newOptions, level + 1);
        if (result) return result;
      }
      return null;
    };
    for (let i = 1; i <= options.length; i++) {
      const nextIndex = (currentIndex + i) % options.length;
      const nextOption = options[nextIndex];
      const nextValue = nextOption.value.toLowerCase().replace(/[^a-z0-9]+/g, '-');
      const testOptions = [...selectedOptions];
      testOptions[optionLevel] = nextValue;
      const validCombination = findValidCombination(testOptions, optionLevel + 1);
      if (validCombination) {
        for (let j = optionLevel + 1; j < allFieldsets.length; j++) {
          const fieldset = allFieldsets[j];
          const option = Array.from(fieldset.querySelectorAll('input[type="radio"]'))
            .find(opt => opt.value.toLowerCase().replace(/[^a-z0-9]+/g, '-') === validCombination[j]);
          if (option) option.checked = true;
        }
        return nextOption;
      }
    }
    return null;
  }
  
  hideInvalidVariantOptions() {
    const allFieldsets = Array.from(this.querySelectorAll('fieldset, select'));
    allFieldsets.forEach((fieldset, fieldsetIndex) => {
      const options = fieldset.tagName === 'SELECT' 
        ? Array.from(fieldset.querySelectorAll('option'))
        : Array.from(fieldset.querySelectorAll('input[type="radio"]'));
      const selectedOptions = this.getSelectedVariant().map(val => this.transformOptionValue(val));
      options.forEach(option => {
        const optionValue = this.transformOptionValue(option.value);
        const variantToCheck = [...selectedOptions];
        variantToCheck[fieldsetIndex] = optionValue;
        let shouldHide;
        if (fieldsetIndex < allFieldsets.length - 1) {
          shouldHide = !this.hasValidCombination(variantToCheck, fieldsetIndex + 1);
        } else {
          shouldHide = !this.isValidVariant(variantToCheck);
        }
        let optionElement, labelElement, swatchDiv;
        if (fieldset.tagName === 'SELECT') {
          optionElement = option;
        } else {
          optionElement = option;
          const optionId = option.getAttribute('id');
          const escapedId = CSS.escape(optionId);
          labelElement = fieldset.querySelector(`label[for="${escapedId}"]`);
          swatchDiv = option.closest('.product-form__swatch');
        }
        if (optionElement) {
          if (!shouldHide) {
            optionElement.style.display = '';
            optionElement.classList.remove('variant-select__hidden');
            if (labelElement) {
              labelElement.style.display = '';
              labelElement.classList.remove('variant-select__hidden');
            }
            if (swatchDiv) {
              swatchDiv.style.display = '';
              swatchDiv.classList.remove('variant-select__hidden');
            }
          } else {
            optionElement.style.display = 'none';
            optionElement.classList.add('variant-select__hidden');
            if (labelElement) {
              labelElement.style.display = 'none';
              labelElement.classList.add('variant-select__hidden');
            }
            if (swatchDiv) {
              swatchDiv.style.display = 'none';
              swatchDiv.classList.add('variant-select__hidden');
            }
          }
        }
      });
    });
  }
  
  hasValidCombination(variantToCheck, startLevel) {
    return this.checkCombinationRecursive(variantToCheck, startLevel);
  }
  
  checkCombinationRecursive(variantToCheck, level) {
    const allFieldsets = Array.from(this.querySelectorAll('fieldset, select'));
    if (level === allFieldsets.length) {
      return this.isValidVariant(variantToCheck);
    }
    const fieldset = allFieldsets[level];
    const options = fieldset.tagName === 'SELECT' 
      ? Array.from(fieldset.querySelectorAll('option'))
      : Array.from(fieldset.querySelectorAll('input[type="radio"]'));
    for (let option of options) {
      const optionValue = this.transformOptionValue(option.value);
      variantToCheck[level] = optionValue;
      if (this.checkCombinationRecursive(variantToCheck, level + 1)) {
        return true;
      }
    }
    return false;
  }

  updateSelectionMetadata({ target }) {
    const { value, tagName } = target;

    if (tagName === 'SELECT' && target.selectedOptions.length) {
      Array.from(target.options)
        .find((option) => option.getAttribute('selected'))
        .removeAttribute('selected');
      target.selectedOptions[0].setAttribute('selected', 'selected');

      const swatchValue = target.selectedOptions[0].dataset.optionSwatchValue;
      const selectedDropdownSwatchValue = target
        .closest('.product-form__input')
        .querySelector('[data-selected-value] > .swatch');
      if (!selectedDropdownSwatchValue) return;
      if (swatchValue) {
        selectedDropdownSwatchValue.style.setProperty('--swatch--background', swatchValue);
        selectedDropdownSwatchValue.classList.remove('swatch--unavailable');
      } else {
        selectedDropdownSwatchValue.style.setProperty('--swatch--background', 'unset');
        selectedDropdownSwatchValue.classList.add('swatch--unavailable');
      }

      selectedDropdownSwatchValue.style.setProperty(
        '--swatch-focal-point',
        target.selectedOptions[0].dataset.optionSwatchFocalPoint || 'unset'
      );
    } else if (tagName === 'INPUT' && target.type === 'radio') {
      const selectedSwatchValue = target.closest(`.product-form__input`).querySelector('[data-selected-value]');
      if (selectedSwatchValue) selectedSwatchValue.innerHTML = value;
    }
  }

  getInputForEventTarget(target) {
    return target.tagName === 'SELECT' ? target.selectedOptions[0] : target;
  }

  get selectedOptionValues() {
    return Array.from(this.querySelectorAll('select option[selected], fieldset input:checked')).map(
      ({ dataset }) => dataset.optionValueId
    );
  }
}

Keep the original element definition

customElements.define('variant-selects', VariantSelects);

Browse other ways to boost conversion rate & profit