Cart & Variant Selection

A bundle goes into the cart as a single line item at the bundle price. The customer's per-component variant choices ride along with it.

Adding to the cart

Post the bundle's purchasableId plus a variant for each component under options[bundleProducts][<productId>]:

<form method="post">
  {{ csrfInput() }}
  {{ actionInput('commerce/cart/update-cart') }}
  {{ hiddenInput('purchasableId', bundle.id) }}
  {{ hiddenInput('qty', 1) }}

  {% for component in bundle.getProducts() %}
    {% set product = component.product %}
    <select name="options[bundleProducts][{{ product.id }}]">
      {% for variant in product.variants %}
        <option value="{{ variant.id }}">{{ variant.title }}</option>
      {% endfor %}
    </select>
  {% endfor %}

  <button type="submit">{{ 'Add to cart'|t }} · {{ bundle.priceAsCurrency }}</button>
</form>

What happens on add

The bundle's populateLineItem() runs the cart service, which:

  1. Resolves the chosen variant for each component (falling back to the product's default variant if the posted one is invalid or missing).
  2. Checks stock: adds a cart error if an inventory-tracked variant can't cover the quantity requested.
  3. Writes readable options to the line item (e.g. Collection: Standard, Skippo Bag: Large) so the cart, checkout and CP order all show titles rather than IDs.
  4. Freezes the selections into the line item snapshot, for the order record and the inventory decrement.

Distinct selections

Different variant selections produce different line-item options, so they stay as separate cart lines; they won't merge quantities.