The selected_or_first_available_variant can return an unavailable variant

Imagine you have a product like this randomly selected Nike shoe that comes in both “Regular” and “Wide”, and you want to hide the unavailable Colors for the “Wide” fit and include 15-18 shoe sizes for the “Wide” fit:

Pretty standard stuff. As a merchant, you don’t make every combination of every sizing option because large feet tend to be wide feet. You don’t make mens size 18 shoes in “pink and blue” because there isn’t enough demand. And you organize your option groups by “upper things constrain lower things”.

As a developer you already know:

  • Product pages have a selected_variant if the product page has a valid variant param.
  • Products in a collection have selected_variant if filters apply to a specific variant
  • The product form does not always have a selected_variant but each option group always has a selected option. There’s no “select a size” group on the server.
  • In a section-rendering-api call each option_group always has a selected value, and that value is determined by the selected_or_first_available_variant logic.

Here’s what you might expect:

  • Passing in an incomplete set of option_values to represent “Fit” or “Color” would combine with selected_or_first_available_variant such that the unspecified option values would select an available variant if one exists.

That is not how it works.

:right_arrow: As far as I can tell, the selected_or_first_available_variant logic runs first and option_value params are applied after.

There is no server logic ensuring that the form state will lead to an available variant when partial option_value params are passed in and there is no way to distinguish option values that only have sold out variants below from options that have no variants below.

What this means:

Product form
If you want to hide “unavailable” variant combinations in a top-down product form (think make/model/year for cars) you cannot treat the product URL with params as the source of truth. You have two choices:

  • A. Untether the state of the product form in the client from the state in the URL/server
  • B. Use iterative section-rendering-api calls to search the space of option_values combinations until you find a combination that leads to a variant or a path that is available

Collection pages
Product cards with “color swatches” cannot accurately represent price, availability, or sold out states if the “color” option group is not the first option_group on the product form.

Proposal

  1. Introduce .exists for option groups to distinguish “sold out” from “valid combination”

product_option_value.available — stays the same

product_option_value.exists — tells you whether that option can lead to a variant

  1. Introduce a new param for ?path=[avaialable,exists]

If the param is not present we keep the current behavior where selected_or_first_available_variant does not consider the currently selected option_values.
This ensures no breaking changes.

Passing in the path tells the server what state to represent for selected_or_first_available_variant

Hey @Gideon, appreciate you flagging this as well.

selected_or_first_available_variant can return a selected variant regardless of availability, and partial option_values selections aren’t guaranteed to be completed into a purchasable combination. However, you’re right that product_option_value.available only represents purchaseability, it doesn’t distinguish between a subtree where every variant is sold out and one where no variants exist.

That makes the proposed exists state a valid capability gap, particularly for sparse variant trees and collection swatches on non-leading options. I’m happy to pass that feedback along on our end for sure.

Could you share a minimal option/variant matrix and one example Section Rendering request showing the unexpected values for selected, available, variant, and selected_or_first_available_variant? That’ll help confirm whether there’s a separate bug in addition to the missing exists state.

For now, available should work when sold-out and nonexistent combinations can be treated the same. Where that distinction matters, maintaining the variant state client-side or iteratively requesting complete combinations appears to be the available workaround, not super ideal though, as you mentioned.

Let me know if I can help clarify anything on our end here and hope to hear back from you soon.

This is a pretty complex thing to describe in words so I set up a toy case here:

There’s links to two test products in the PR description and here.

Just from a theoretical standpoint, if you want “top-down” selection UX that hides unavailable options, and you want a stateless client, the section-rendering-api request has to stop at the option value that was just clicked and let the server determine how the upper values change the state of lower options.

In that phone case example product linked above, imagine the form is in the default state and you think “I have an iPhone 17 so I’ll click that”:


This is where you can see why adding .exists is necessary but not sufficient.

The server response will keep Purple selected even though Air is the only option that comes in Purple.

Horizon serializes the current state for the request, so this is a little hard to see but the question is:

What happens when I pass in partial option_values like ?option_values=iPhone,17.

There is no param combination I can pass in that will prevent purple from being selected, and there’s no way for the client to know which values of [purple,yellow,brown,black] contain variants for [iPhone,17] until the server returns the form with the 17 selection step made.

The naive JS example in the PR “reclicks” until it lands in a valid state. I don’t think there is a way to implement this UX pattern on high-variant products that isn’t essentially “reclick once per option group”.

This is why I think we need to change the default server response to run option_values before determining the selected_or_first_available_variant for a partial option_value param combination or we need the api to support an additional param like ?path=[avaialable,exists].

Perhaps a better API redesign would be something like ?option_values=123,456,avaialable and ?option_values=123,456,exists where you pass a keyword directly into the ID slot.

Ideally we would have support for [<option_value_id_N>|available|exists|unset] but unset is a bigger topic for another thread.

Thanks for hearing me out. I know this is a lot of detailed info, and this is difficult to test and replicate.

Thanks for this @Gideon - one last thing that would help me isolate the server-side behaviour: could you share one exact Section Rendering request using partial option_values, along with the UTC timestamp and x-request-id if available?

An abbreviated response showing the selected option values and selected_or_first_available_variant would be enough, plus what you expected it to select instead. No worries if not, this would just help with investigating things on my side here. Really appreciate your breakdown, happy to look into this.