Skip to main content

Support high-variant products

Themes have historically used the Liquid API to load all of a product's variants when rendering a product. However, as variant counts increase, this pattern results in a decline in performance. This is especially critical for products with more than 250 variants and combined listings.

To prevent poor render performance in themes, we've restricted product.variants to return a maximum of 250 variants and added APIs to solve common use cases that previously required loading all variants. To prepare for increased variant limits, audit your theme for any code that might rely on all product variants to be present.

This guide helps you support high-variant products in your theme by outlining new APIs, identifying code patterns that might not scale with increased variant limits, and providing a walkthrough for how to construct an option value picker.


We've added two APIs to support building themes for high-variant products: a product_option_value object and granular option value selection for product URLs.

Anchor to [object Object], objectproduct_option_value object

The product_option_value object provides rich contextual information about each option value based on the active variant selection.

FieldApplication
availableWhether the option value has any available variants, in the context of the selected values for previous options. Enables themes to display a "top-down" availability UX, where an option value is available if any part of its subtree is purchasable.
idOption value IDs should be used with granular option value selection when re-rendering the option values section.
variantThe variant that's associated with this option value, combined with the other currently selected option values (if one exists). In addition to providing trivial access to the variant for the option value, might also simplify option value availability through the variant's available field.
selectedWhether the option value is selected. May be used instead of a product_option.selected_value == option_value check.
product_urlThe URL of the product associated with this option value. Used by combined listing products to indicate that selecting the option value should load another product’s content.

Anchor to Granular option value selection for product URLsGranular option value selection for product URLs

The key change to support more variants is to defer loading variant information until it's needed. To facilitate this, we now support a option_values parameter on product page URLs. In combination with the product_option_value Liquid object, clients can load only the variants that are relevant to the selected option value state.

GET /products/pants-green?option_values=<option_value_id_1>,...,<option_value_id_N>
Caution

If the option combination has no associated variant, then both product.selected_or_first_available_variant and product.selected_variant return null.


Anchor to Spotting Problematic Code PatternsSpotting Problematic Code Patterns

Any Liquid code that requires a complete set of product variants won't work as expected if the product contains more than 250 variants. For example, while the the following Liquid code intends to iterate through all variants for the product to render a hidden variant input in the product form, it actually only iterates through the first 250 variants:

{% form 'product', product, id: product_form_id %}
<select name="id" style={{display: 'none'}}>
{%- for variant in product.variants -%}
<option
value="{{ variant.id }}"
{% if product.selected_or_first_available_variant.id == variant.id %}
selected="selected"
{% endif %}
>
{{ variant.title }}
</option>
{% endfor %}
</select>
{% endform %}
Note

Not all variant-related code needs to change. For example, product.variants.first or product.variants.size > 1 will still work because they don't rely on the full variant set.

Anchor to How to update your themeHow to update your theme

To support high-variant products, we recommend you audit your theme for usage of product variants and product | json. Look for opportunities to eliminate overfetching or defer loading variant information until needed, using the new APIs, Storefront API, or Ajax API.

Note

Themes are no longer required to support users that have disabled JavaScript. You can remove any code to handle this.

If these solutions don't work for your use case, then please share your feedback in the Shopify Developer Community Forum.


Anchor to Building an option value picker for high-variant productsBuilding an option value picker for high-variant products

The most common pattern that requires loading all variants is rendering an option value picker. The key areas that this guide covers are

Anchor to Rendering option valuesRendering option values

Themes should use product.options_with_values for rendering options, and the new product_option_value object returned by product_option.values for option values.

{%- for option in product.options_with_values -%}
{%- for option_value in option.values -%}
<input
id="{{ section.id }}-{{ option.position }}-{{ forloop.index0 -}}"
type="radio"
name="{{ option.name }}-{{ option.position }}"
value="{{ option_value | escape }}"
{% if option_value.selected %}
checked
{% endif %}
{% unless option_value.available %}
className="disabled"
{% endunless %}
/>
<label for="{{ section.id }}-{{ option.position }}-{{ forloop.index0 -}}">
{{ option_value -}}
</label>
{%- endfor -%}
{%- endfor -%}
Tip

Refer to Getting started with swatches for Shopify themes to add support swatches in your option value picker.

Anchor to Option value availabilityOption value availability

Instead of iterating over all product variants, you can determine option value availability using product_option_value.available or variant.available.

Using product_option_value.available results in a "top-down" UX where option values are treated as a tree, and an option value is available if any part of the subtree is purchasable.

Using variant.available results in an "adjacent" UX where option values are treated as graph nodes, and an option value is available if the node is purchasable.

When a buyer selects an option value, use the product_option_value.id to make a request to the Section Rendering API to load and display the next option value availability state.

Building on the previous example, the following code sets option value availability and refreshes the section with the new availability state when an option value is selected:

<input
...
{% unless option_value.available %}
className="disabled"
{% endunless %}
data-option-value-id="{{ option_value.id }}"
/>
...
<script>
function onVariantChange(event) {
// Assumes the option value picker is wrapped in a section with a class optionValueSelectors with a section-id data attribute
const optionValueSelectors = event.target.closest('.optionValueSelectors');
const sectionId = optionValueSelectors.dataset.sectionId;

// Get the selected option value IDs and format as query param
const selectedOptionValues = Array.from(
optionValueSelectors.querySelectorAll('input[type="radio"]:checked')
).map(({dataset}) => dataset.optionValueId);

const params = selectedOptionValues.length > 0
? `&option_values=${selectedOptionValues.join(',')}`
: '';

// Fetch the product section with the new option value selection, and replace the option value picker with the new availability state
fetch(`${window.location.href}?section_id=${sectionId}${params}`)
.then((response) => response.text())
.then((responseText) => {
const html = new DOMParser().parseFromString(responseText, 'text/html');

optionValueSelectors.innerHTML = html.querySelector('.optionValueSelectors').innerHTML;
document.querySelector(`#${event.target.id}`).focus();
});
}
</script>

Anchor to (Optional) Supporting combined listings(Optional) Supporting combined listings

To support combined listings, themes should load and replace product information when switching between sibling products. You can do this with the product_option_value.product_url property.

For example, a shop has a combined listing Pants with child products Pants - red and Pants - green. When a buyer lands on Pants and changes the color, then the UI should update to display information from the corresponding child product, such as price, images, and description.

<!-- Add product url to option value input -->
<input
type="radio"
id="option_value_1"
name="{{ option.name }}"
value="{{ option_value | escape }}"
data-product-url="{{ option_value.product_url }}"
...
/>
...
<script>
...
function onVariantChange(event) {
const {productUrl} = event.target.dataset;

// Fetch the product for the selected option value
fetch(`${productUrl}?section_id=${sectionId}${params}`)
.then((response) => response.text())
.then((responseText) => {
// Update the product section and any related sections
});
}
</script>

Anchor to Example option value pickerExample option value picker

Putting it all together, a theme can load a minimal set of variants using the product_option_value.variant object, use the Section Rendering API to load the next availability state by passing the new option value selection, and support combined listings by replacing product content when a buyer switches to a sibling product:

<section id="product" data-product-url="{{ product.url }}" data-section-id="{{ section.id }}">
<h2>{{ product.title }}</h2>
<div className="price">
{{ product.price | money }}
</div>
<!-- ...other product info... -->

<div className="optionValueSelectors">
<!-- Use options_with_values to render options -->
{%- for option in product.options_with_values -%}
<fieldset className="option">
<legend>{{ option.name }}</legend>

{%- for option_value in option.values -%}
{%- capture input_id -%}
{{ section.id }}-{{ option.position }}-{{ forloop.index0 -}}
{%- endcapture -%}

<!-- Use product_option_value` fields to simplify option value input rendering and support combined listings -->
<input
type="radio"
id="{{ input_id }}"
name="{{ option.name }}"
value="{{ option_value | escape }}"
{% if option_value.selected %}
checked
{% endif %}
{% unless option_value.available %}
className="disabled"
{% endunless %}
data-product-url="{{ option_value.product_url }}"
data-option-value-id="{{ option_value.id }}"
/>

<label for="{{ input_id }}">
{{ option_value -}}
</label>
{%- endfor -%}
</fieldset>
{%- endfor -%}
</div>
</section>

{% javascript %}
...
function onVariantChange(event) {
const productElement = event.target.closest('#product');
const sectionId = productElement.dataset.sectionId;

// Combined listings: We use old and new product urls to determine if the product should change
const oldProductUrl = productElement.dataset.productUrl;
const newProductUrl = optionValueElement.dataset.productUrl;

// Get the selected option value IDs and format as query param
const selectedOptionValues = Array.from(
optionValueSelectors.querySelectorAll('fieldset input:checked')
).map(({dataset}) => dataset.optionValueId);
const params = selectedOptionValues.length > 0
? `&option_values=${selectedOptionValues.join(',')}`
: '';

// Deferred variants: Fetch the option value picker with the new availability state when remaining on the same product
// Combined listings: Fetch the product associated with the selected option value and replace the entire product if switching to a sibling product
fetch(`${newProductUrl}?section_id=${sectionId}${params}`)
.then((response) => response.text())
.then((responseText) => {
const html = new DOMParser().parseFromString(responseText, 'text/html');

// Combined listings: If the product changed, replace the old product section with the new product section
if (newProductUrl && oldProductUrl !== newProductUrl) {
productElement.parentNode.insertBefore(html.querySelector('#product'), productElement);
productElement.remove();

// Focus the input for the last clicked option value
document.querySelector(`#${event.target.id}`).focus();

return;
}

const optionValueSelectors = event.target.closest('.optionValueSelectors');
optionValueSelectors.innerHTML = html.querySelector('.optionValueSelectors').innerHTML;
document.querySelector(`#${event.target.id}`).focus();

// Update any other sections that depend on the option value picker
});
}
{% endjavascript %}

Was this page helpful?