Type: Fix / Developer Guide · Stage: Optimize · Topic: Liquid / Variants / Theme Architecture
On this page
Quick answer
Shopify supports up to 2,048 variants per product, but the Liquid product.variants array returns a maximum of 250. If a custom picker loops through product.variants to build every size/color/finish combination, the picker can silently omit valid variants once the product grows. This is not a broken import or missing inventory. It is a theme architecture problem.
Build option selectors from product.options_with_values and the product_option_value object. Resolve only the selected combination and refresh product information through Shopify’s Section Rendering API using the option_values URL parameter. Avoid embedding a giant variant JSON payload in the page.
Why old Shopify variant tutorials stopped scaling
For years, a standard theme customization was to loop through product.variants, output a hidden <select name="id">, and write a JSON object of every variant into the page. That worked when products had at most 100 variants. Shopify raised the product limit to 2,048 on October 15, 2025, but did not turn Liquid into a way to load 2,048 variant records into every product page.
Shopify intentionally caps product.variants at 250 to protect render performance. The product can have 600 legitimate variants while a theme’s loop exposes only 250. The admin product count and the storefront picker are therefore not measuring the same thing.
The three-options-per-product limit also remains. Increasing the variant count does not create a fourth native option axis, and it does not make free-text engraving a variant. Those are different modeling problems.
Find the code that assumes every variant is loaded
In Online Store → Themes → Edit code, inspect product form snippets, variant picker snippets, product card snippets, quick-add components, and any custom JavaScript used by bundles or configurators. Search for these patterns:
for variant in product.variants
product.variants | json
product | json
product.variants.size
name="id"
The presence of product.variants is not automatically a bug. Code such as product.variants.first can be harmless. The red flag is code that assumes the array contains every variant, especially if it builds a complete dropdown, checks every possible combination, or looks up an arbitrary SKU by iterating over the array.
Also inspect JavaScript that caches a page-level variant array. An otherwise modern Liquid picker can still fail if the price updater or Add to cart handler searches a truncated JSON array.
Render option values instead of every combination
Shopify’s product_option_value object exposes an option value’s ID, whether it is selected, its contextual availability, an associated variant where one exists, and a product_url for combined listings. A simplified picker can begin with:
<div class="option-value-selectors" data-section-id="{{ section.id }}">
{%- for option in product.options_with_values -%}
<fieldset>
<legend>{{ option.name | escape }}</legend>
{%- for option_value in option.values -%}
<label>
<input
type="radio"
name="option-{{ option.position }}"
value="{{ option_value | escape }}"
data-option-value-id="{{ option_value.id }}"
data-product-url="{{ option_value.product_url }}"
{% if option_value.selected %}checked{% endif %}
>
{{ option_value | escape }}
</label>
{%- endfor -%}
</fieldset>
{%- endfor -%}
</div>
Illustrative Liquid only: production code still needs accessible IDs, disabled/availability presentation, form state, loading/error handling, and a JavaScript controller. Do not paste the snippet into a live theme without integrating it with the theme’s product form.
Notice what is missing: no loop over product.variants and no giant variant JSON blob.
Resolve a selected combination on demand
- Read the selected option-value IDs from the picker’s checked inputs, in option order.
- Request the current product section with
?section_id=SECTION_ID&option_values=ID1,ID2,.... Preserve any existing URL query parameters when constructing the request. - Parse the returned section HTML and replace the appropriate picker, price, media, SKU, and product-form state. The exact replacement targets depend on the theme.
- Read the resolved variant from the newly rendered product context. Shopify warns that
product.selected_variantandproduct.selected_or_first_available_variantcan both benullif the chosen combination does not exist. - Set the product form’s variant ID only when a real variant is selected. Disable Add to cart and show a useful message when the combination is invalid or unavailable.
For example, the URL shape is /products/example?section_id=main-product&option_values=123,456,789, where those numeric values are option-value IDs, not variant IDs. Shopify’s Section Rendering API returns HTML, not a list of all product variants.
Combined listings need one more step: when an option value has a different product_url, the selected color or style may belong to another product. Fetch that product’s section and update product-specific content, rather than treating it as another variant of the current product.
Keep the cart and price in sync
A picker can look correct while submitting the wrong variant. After a section refresh, verify the product form’s name="id" value, the displayed price and compare-at price, availability, inventory messages, selling-plan state, and media. If the theme uses a cart drawer or quick add, test those entry points separately.
Do not infer availability from the first 250 variants. product_option_value.available is contextual: it answers whether an option value has a purchasable path given earlier selected options. For exact combination validation, check the resolved variant itself.
Common misunderstanding
“Shopify increased the limit, so every existing theme supports 2,048 variants.” The platform data limit changed; the theme’s old variant-loading strategy may not have. Apps and sales channels can have their own compatibility limits too.
“Just raise the Liquid loop limit.” A limit: 2048 parameter cannot override the product.variants cap. Fetch the right data when the customer selects an option.
“We need a product-options app for the fourth option, so the 250 cap does not matter.” Custom line-item properties and native variants solve different problems. A product-options app does not automatically fix truncated Liquid variant data in a custom theme.
How to test this
- Use a test product with more than 250 variants, including a purchasable combination whose variant is beyond the first 250 returned by Liquid.
- Select early, middle, and late option combinations. Confirm the picker exposes the correct option values.
- Inspect the Section Rendering request and confirm the
option_valuesparameter contains option-value IDs, not variant IDs. - Verify the displayed SKU, price, media, availability, and submitted
idafter each selection. - Test a nonexistent combination: the selected variant may be null and Add to cart must not submit a stale ID.
- Test a sold-out variant, a product with fewer than 100 variants, a quick-add form, and a combined-listing sibling if used.
- Measure page weight and time-to-interactive before and after removing all-variant JSON payloads. Check mobile performance, not only desktop.
Sources and further reading
- Shopify Dev: Support high-variant products (250-variant Liquid cap and option-value APIs)
- Shopify Dev: Defer child product loading in combined listings
- Shopify Help: Add variants (2,048 variants and three options)
- Shopify: Introducing 2,048 Product Variants (October 15, 2025)
Documentation checked October 14, 2026.

