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:
- Resolves the chosen variant for each component (falling back to the product's default variant if the posted one is invalid or missing).
- Checks stock: adds a cart error if an inventory-tracked variant can't cover the quantity requested.
- 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. - 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.