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);