Free for Craft Commerce 5
Sell several things as one, and ship them as many
Every other bundle plugin for Craft sells a bundle as one line item, and a single line can never take stock off the right shelves, be partly shipped, or be taxed correctly when its contents are taxed differently. Present puts a box in the cart as a container line plus one real line item per component — the shape WooCommerce Product Bundles uses, for the same reasons.
Features
A bundle that Commerce's own stock, tax, shipping and fulfilment all understand — because to Commerce, its contents are just line items.
Real line items
A box is a container line plus one line item per component. Commerce commits stock per purchasable and records fulfilment per line item, so the components get both — correctly, even when the same variant is also in the cart on its own.
- Each component keeps its own tax category, shipping category and weight
- Two identical boxes merge into one line at quantity two
- A box that has come apart is repaired or removed on the next recalculation
Sells out when its contents do
A bundle holds no stock of its own. Its availability is the fewest boxes any one component can cover, so a box sells out when the tea runs low — without anyone editing it.
- Untracked and sell-out-of-stock mean unlimited, not zero
- The same variant in two slots is counted once
- A Buildable column on the bundles index, and a console check across every bundle
Pricing that adds up
Per-item pricing charges each component at its own sale price, with the bundle price as a fee on top. Fixed pricing spreads one price across the components by line value, in minor units, with largest-remainder allocation — so the parts add up to exactly the price, every time.
- Slot and option discounts, where null means "not set" and 0 means "none"
- A catalog sale on a component carries into the box
- Put a fixed price on the bundle line instead (Pro)
Build your own box
Pro lets the shopper fill it: quantity ranges per slot, a minimum and maximum for the whole box, optional slots, no-repeat pools, and pools drawn from a product type or a relation rather than a list.
- A drop-in builder in one Twig tag, with no dependencies
- A JSON API if you would rather build your own
- The preview and the checkout run the same validation
How many boxes went out?
Pro rolls Commerce's own inventory movements back up into an answer about boxes: a box ships when everything in it has. The order screen shows every component, what is outstanding, and what is holding the rest up.
- Unfulfilled, partly fulfilled and fulfilled, per box
- Blockers, worst first: "four mugs short of three boxes"
- Nothing stored, so it cannot drift from what the warehouse did
Ignored, not obeyed
Pro settings on a Lite install stay in the database untouched, and the control panel says which ones are being ignored. A store that lapses stops selling boxes it can no longer validate, rather than selling them wrongly — and upgrading restores everything.
- Range slots behave as fixed, query pools fall back to their list
- Allocation falls back to spreading
- Nothing is deleted on a downgrade
Three lines to a box builder
One tag renders pickers for every slot with live pricing and live stock. Because the contents are real line items, the cart needs one more loop to fold them under their box.
{% set bundle = craft.present.bundle('GIFTBOX') %}
<h1>{{ bundle.title }}</h1>
{# Pickers for every slot, live pricing, live stock, min/max enforced #}
{{ craft.present.builder(bundle) }}
{# In the cart: fold each box's contents under it #}
{% for item in craft.present.cartLineItems(cart) %}
{% if craft.present.isComponent(item) %}
<tr class="component"><td colspan="2">↳ {{ item.qty }} × {{ item.description }}</td></tr>
{% else %}
{% set group = craft.present.group(item) %}
<tr>
<td>{{ item.qty }} × {{ item.description }}</td>
<td>{{ (group ? group.total : item.total)|commerceCurrency(cart.currency) }}</td>
</tr>
{% endif %}
{% endfor %}
Frequently Asked Questions
The questions worth answering before you install it.
Lite is, and it is a complete bundle plugin: bundle types and bundles, fixed slots with a list of choices, per-item and fixed pricing, slot and option discounts, the stock rollup and the Twig API. Pro is $99 with a $79/year renewal, and adds build-your-own-box, query-driven pools, allocation choice, edit-in-cart, partial fulfilment, the JSON API and builder, and the CP configurator.
Because a variant is one thing with one SKU and one stock figure. If "Gift Box" is a variant, you have to keep its stock in step with three other products by hand, you cannot ship half of it, and selling one does not take a mug off the shelf.
Because that is what makes the rest of it possible. Commerce commits stock per purchasable and records fulfilment per line item. Components that are real line items get both, plus their own tax categories, shipping categories and weights. A bundle sold as a single line item cannot do any of that.
It will list a box's contents as separate rows until you tell it not to. Two helpers fix that — craft.present.isComponent() and craft.present.cartLineItems().
By default it is spread across the component line items in proportion to their value, so each keeps its own tax category and a box of mixed-rate goods is taxed correctly. With Pro you can put it all on the bundle line instead, which is simpler to read on an order and right when everything in the box shares a tax category.
Yes. Components are shippable line items carrying their own weights and shipping categories, so weight-based and table-rate plugins see a box exactly as they would see its contents bought separately. The container line ships only if you give the bundle a weight of its own.
Yes, on the container and the components alike, since they are ordinary line items. Present clears a component's promotional price, because a component inside a box is sold at the box's price and a promotional price underneath would undercut the bundle.
No. The stock rollup would have to recurse, the fulfilment rollup would have to nest, and Commerce would still see one flat list of line items at the end of it. Present refuses rather than half-supporting it.
Pro settings are ignored, not obeyed. Range slots behave as fixed, query pools fall back to their configured list, allocation falls back to spreading, item limits stop applying, and the control panel flags which bundles are affected. Nothing is deleted, and upgrading restores it.
Craft CMS 5.3+, Craft Commerce 5.0+ and PHP 8.2+.
Start with Lite, add a shopper when you need one
Lite is free and sells fixed bundles properly — real line items, the stock rollup and both pricing modes. Pro is $99 with a $79/year renewal, and adds build-your-own-box, partial fulfilment, the JSON API and builder, and the CP configurator.