Skip to content
heapbyte - A name of excellence

Architecture · 9 September 2026

Pricing by dimensions: when Shopify variants are the wrong model

There are two kinds of product on Shopify, and almost every difficulty with variants comes from treating the second as though it were the first. The first is enumerable: however many combinations exist, you could write them all down. The second is computed — the price is a function of what the customer types, and there is no list. A curtain cut to 2,347mm by 1,780mm is not one of a set of curtains. If that is what you sell, no increase to the variant limit will ever help you, and the useful question is not how to fit your product into variants but what to use instead.

8 min read
Written by the HeapByte engineering team

Pricing by dimensions: when Shopify variants are the wrong model

Two kinds of product

The test is one question: can you write down every purchasable combination?

If yes, you have an enumeration problem. Three sizes, twelve colours and four materials is 144 rows. Tedious to manage, but finite, and Shopify's variant model is the right home for it. As of October 2025 you have 2,048 variants per product to work with, so the ceiling that used to force awkward workarounds has probably stopped being your problem.

If no — if the answer is "it depends what they enter" — you have a computed product, and no number of variants is the answer, because the set is not finite. A shelf priced by depth and length in fractional increments has no variant count. Neither does a made-to-measure blind. The question was never how many variants you are allowed.

Everything below is for the second case.

Why the limit increase did not help you

Worth being accurate about what changed, because it did change.

The per-product variant ceiling went from 100 to 2,048 on 15 October 2025, for all merchants on all plans, with nothing to enable. Combined Listings, on Shopify Plus, lets you present several separate products as one listing joined by a shared option, so each child keeps its own URL, description and images.

Both scale enumeration. Neither creates the thing a computed product needs.

Two constraints show why. The first: the three-option limit did not move. A product still has at most three options, whatever the variant count. Width, height, fabric, heading, lining and rail is six axes; the fourth already had nowhere to go at 100 variants and still has nowhere to go at 2,048.

The second is the one nobody mentions: the media limit did not move either. A product is capped at 250 media items across images, video and 3D, and Shopify has said plainly that raising it was not part of the variant change and that the cap cannot be raised. So a product can carry 2,048 variants and 250 pictures. Past 250 you cannot show a distinct image per variant at all. The ability to enumerate outran the ability to represent what you enumerated — which is a fair summary of why enumeration was the wrong axis to scale.

Where the model actually breaks

Five patterns. One of them is probably yours.

Continuous inputs. A dimension in millimetres is not a discrete set. You can force it into one by offering 10mm steps, and you have then either restricted what the customer can buy or created a variant list you cannot maintain.

More than three axes. The three-option limit binds regardless of variant count. Once the product genuinely has four or more independent choices, option-splitting tricks and concatenated option values start encoding meaning into strings, and every downstream system — reporting, fulfilment, ERP — has to parse it back out.

Price as a formula rather than a lookup. Area, perimeter, material rate, wastage allowance, minimum charge. A variant price is a stored number. If yours is the output of a calculation, storing it means storing every possible output.

Conditional availability. A fabric only offered above a certain width; a finish incompatible with a material. Variants have no concept of one option constraining another, so the rule lives in the theme, and nothing prevents an invalid combination reaching the cart by another route.

Inventory consumed in units of measure. You do not hold twelve of a 2,347mm curtain. You hold a roll, and each order consumes area or length from it. Shopify's inventory model counts pieces, so if the thing being depleted is measured rather than counted, variant-level inventory is tracking the wrong noun.

Custom Floating Shelves is a worked example of several of these at once — price driven by material, width, depth, thickness, finish and hardware, with fractional dimensions and material-specific restrictions on which finishes and product types are available. That rebuild documents the architecture.

What the architecture looks like instead

Four decisions, and the honest answer to each depends on your plan.

Where configuration state lives. Not in variants. The rule set — rates, valid ranges, compatibility, minimums — belongs somewhere versioned and editable without a deploy: metafields or metaobjects if the merchant maintains it, an external service if it is genuinely complex or shared with production. The storefront reads it and computes; it does not own it.

How the computed price reaches the cart. This is where plan matters and where most write-ups are vague. The clean mechanism is the Cart Transform Function API, whose update operation overrides a cart line's price, title and image — but that operation is Plus-only. Below Plus you are choosing among less tidy patterns: creating variants on the fly through the Admin API, a price-per-unit variant multiplied by quantity, or moving the transaction to a draft order. Each has a real cost — catalogue pollution, a quantity field that no longer means quantity, or a checkout that leaves the standard flow. There is no pattern here that is free.

How the specification survives to fulfilment. Line item properties. They are added on cart/add.js, they persist onto the order as customAttributes, and a leading underscore marks a property the theme hides from the customer while fulfilment still sees it.

js
async function addConfiguredItem(variantId, spec) {
  const res = await fetch(`${window.Shopify.routes.root}cart/add.js`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Accept: "application/json" },
    body: JSON.stringify({
      items: [
        {
          id: variantId,
          quantity: 1,
          properties: {
            // Customer-facing: shown in cart and on the order confirmation.
            Width: `${spec.widthMm} mm`,
            Height: `${spec.heightMm} mm`,
            Fabric: spec.fabricName,
            Heading: spec.headingName,
            // Private: carried through to fulfilment, hidden by the theme.
            _spec_version: "3",
            _fabric_sku: spec.fabricSku,
            _area_m2: spec.areaM2.toFixed(4),
            _price_basis: spec.priceBasis, // "area" | "perimeter" | "minimum"
            _config_id: spec.configId,
          },
        },
      ],
    }),
  });
  if (!res.ok) throw new Error(`cart/add failed: ${res.status}`);
  return res.json();
}
The documented carrier for a computed specification. Underscore-prefixed keys are the convention for values fulfilment needs and the customer should not see.

The underscore is a convention, not enforcement

One warning that catches people out. Shopify's documentation is explicit that private properties are still available in the Liquid line_item.properties object and the Ajax API — to hide them on the storefront you must modify the theme. If your theme does not filter, your internal SKU codes render in the cart. This is the filter that makes the convention true:

liquid
{% for property in line_item.properties %}
  {%- assign first_char = property.first | slice: 0 -%}
  {% unless property.last == blank or first_char == '_' %}
    <div class="line-item-property">
      <dt>{{ property.first }}</dt>
      <dd>{{ property.last }}</dd>
    </div>
  {% endunless %}
{% endfor %}
Without this, every private property renders in the cart.

What operations receives

The order line should be readable by a person who was not in the conversation. Dimensions with units, the resolved material, and enough identifiers to reproduce the calculation — a rule-set version, a configuration id — so that a price queried three months later can be explained rather than defended.

Vorhang Schweiz treats configuration detail as order data for exactly this reason: the numbers the customer entered are what the workshop cuts to. This is what an integration reads off the order.

graphql
query OrderSpecification($id: ID!) {
  order(id: $id) {
    name
    createdAt
    lineItems(first: 50) {
      edges {
        node {
          title
          quantity
          sku
          originalUnitPriceSet { shopMoney { amount currencyCode } }
          customAttributes { key value }
        }
      }
    }
  }
}
customAttributes carries both halves, private keys included. Written against Admin API 2026-07.

When an app is genuinely the right answer

Configurator apps solve a real problem and solve it well. If your option set is manageable, your pricing is a lookup or a simple surcharge, and nothing downstream needs the configuration beyond the packing slip, an app is the correct answer and building is a waste of money.

They stop being the answer at a predictable boundary: when pricing becomes a formula with business rules rather than a table, when validity depends on combinations rather than individual choices, or when the configuration has to arrive intact in a production or ERP system in a shape that system already understands. At that point the app is still doing its job — it is just no longer the same job.

When this needs an engineer

If the answer to the enumeration question was yes, you do not need us. Restructure the catalogue, use the headroom you now have, and move on.

It needs an engineer when the price is computed rather than stored, when validity rules constrain combinations, when the configuration has to reach a production system in a specific shape, or when the thing your inventory depletes is measured rather than counted. That is product configurator work, and it starts with modelling the rules, not with picking an app.

Where to start

Write down every purchasable combination of your product. If you finish, you have an enumeration problem and Shopify has already raised the ceiling you were hitting. If you cannot start, the model is the problem.

Send us the product and the pricing rule.

Insights

Apply this to your store.

An audit turns the general principle into a specific list of changes, ordered by what actually pays back.