Shopify supports product bundles — grouping multiple products or variants into a single sellable unit. This page helps developers choose the right bundle architecture for a Hydrogen storefront and implement it end to end.
Bundle Types at a Glance
| Aspect | Product Fixed | Variant Fixed | Customized |
|---|---|---|---|
| Use case | Standard bundles with shared options (e.g., scent + size) | Combined listing components, multi-packs, explicit variant-to-component mapping | Mix-and-match where customers choose components |
| Component limit | 30 | 30 | 150 |
| Option limit | 3 | 3 | 3 |
| Inventory | Automatic oversell protection (Shopify-managed) | Automatic oversell protection (Shopify-managed) | App-managed on storefront; post-add-to-cart checking built in |
| Pricing | Parent-derived, allocated to components via weighted algorithm | Parent-derived, allocated to components via weighted algorithm | Expand mode: parent-derived; Merge mode: component-derived with price adjustment in function |
| Storefront rendering | Works without Liquid changes | Works without Liquid changes | Requires theme app blocks or custom picker UI |
| Custom storefront publishing | Supported (requires enablement) | Not supported | Requires app-managed UI |
| Complexity | Low — API mutations only | Low — two-step variant association | High — requires a Cart Transform Shopify Function (Rust or JS/TS) |
When to Use Each
Product Fixed Bundles — best default choice. Use when the merchant defines bundles with shared options (e.g., a “Hair and Skin Bundle” where the customer picks Scent and Shampoo Size). Components are configured at the product level and Shopify auto-maps variant combinations. Lowest implementation cost.
Variant Fixed Bundles — required when bundle components are combined-listings children, because combined listing children are separate products and product-fixed bundles would attach all children to every variant regardless of option selection. Also appropriate for multi-packs or any case where each variant maps to an explicit set of component variants. Configured at the variant level. Note: cannot be published to custom storefronts, so not usable in Hydrogen unless rendering is handled without the publishing API.
Customized Bundles — use only when the customer must choose their own components (mix-and-match). Requires building a Cart Transform Function, managing component data via metafields or line properties, and building a picker UI. Significantly more work; only justified when the product experience demands it.
Default recommendation
For most Hydrogen projects, start with Product Fixed Bundles. They require no Shopify Functions, no custom storefront UI beyond detection and display, and support custom storefront publishing. Use Variant Fixed when bundle components are combined-listings children (product-fixed won’t work — see below). Only move to Customized Bundles if the client’s product model requires customer-chosen components.
Variant-Fixed Bundles with Combined Listings
When bundle components come from combined-listings, product-fixed bundles don’t work — they attach every component product to every bundle variant, making option selection meaningless. Both variant-fixed and customized bundles handle this correctly, but variant-fixed is the simpler option (no Shopify Functions or custom picker UI required). (Source)
Example — “Lip Kit” bundle with two combined listings (Lip Products: Matte Lip, Gloss Lip; Liner Products: Red Liner, Nude Liner):
| Bundle variant | Product-fixed (wrong) | Variant-fixed (correct) |
|---|---|---|
| Matte Lip / Red Liner | Matte Lip + Gloss Lip + Red Liner + Nude Liner | Matte Lip + Red Liner |
| Matte Lip / Nude Liner | Matte Lip + Gloss Lip + Red Liner + Nude Liner | Matte Lip + Nude Liner |
| Gloss Lip / Red Liner | Matte Lip + Gloss Lip + Red Liner + Nude Liner | Gloss Lip + Red Liner |
| Gloss Lip / Nude Liner | Matte Lip + Gloss Lip + Red Liner + Nude Liner | Gloss Lip + Nude Liner |
How Combined Listings Map to Bundle Options
- Combined listing parent → component group (one bundle option)
- Each child product → option value within that group
- Children’s own variant options (e.g., Shade, Size) → secondary options on the bundle
- Regular products can also be added as single-child component groups
When multiple groups contribute options with the same name and identical values, they merge into one dimension. Same name with different values are kept separate.
When a child lacks a variant for a secondary option value, that combination is excluded from the bundle product (invalid combination).
Bundle Pricing (Variant-Fixed)
price = sum(childVariantPrice × groupQuantity) for each component groupEach component group has a configurable quantity that affects both pricing and variant relationships. For groups with secondary options, the child variant matching the secondary selections determines the price.
Save Flow (Variant-Fixed)
Two-step mutation sequence:
productSet(withsynchronous: true) — creates/updates the bundle product with options and variants.productVariantRelationshipBulkUpdate— wires each bundle variant to its component variant IDs. Must run after step 1 because it references the returned variant IDs.
Both must succeed. If productSet succeeds but relationships fail, the bundle
has empty variants with no components.
Storefront Detection
Bundles are detected via the requiresComponents boolean on ProductVariant.
Query this through an aliased field to keep bundle logic separate from variant
selection:
isBundle: selectedOrFirstAvailableVariant(
ignoreUnknownOptions: true
selectedOptions: { name: "", value: "" }
) {
...on ProductVariant {
requiresComponents
components(first: 100) {
nodes {
productVariant { ...ProductVariant }
quantity
}
}
groupedBy(first: 100) {
nodes { id }
}
}
}At runtime: const isBundle = Boolean(product.isBundle?.requiresComponents);
Storefront Components
BundleBadge
A visual label (“BUNDLE”) rendered on product images and cart line items.
Typically absolute-positioned — requires a parent with position: relative.
BundledVariants
Renders the list of products contained in a bundle. Each row shows a thumbnail,
product/variant title, and quantity, linking to the individual product page.
Accepts variants: ProductVariantComponent[] from the Storefront API types.
Implementation Outline (Fixed Bundles)
These are the typical files touched when adding fixed bundle support to a Hydrogen storefront, based on Shopify’s skeleton template. Adapt paths to your project structure.
| Area | Files | Changes |
|---|---|---|
| GraphQL fragments | app/lib/fragments.ts | Add requiresComponents, components, groupedBy to cart fragments |
| Product page | app/routes/products.$handle.tsx | Query isBundle, render BundledVariants, pass isBundle to ProductImage and ProductForm |
| Collection page | app/routes/collections.$handle.tsx | Query isBundle in PRODUCT_ITEM_FRAGMENT |
| Cart | app/components/CartLineItem.tsx | Show BundleBadge for bundle line items |
| Product form | app/components/ProductForm.tsx | Conditional button text: “Add bundle to cart” vs “Add to cart” |
| Product image | app/components/ProductImage.tsx | Show BundleBadge overlay when isBundle |
| Product card | app/components/ProductItem.tsx | Show BundleBadge on product cards in listings |
| Styles | app/styles/app.css | Add position: relative to .product-image |
Steps are not strictly ordered; dependencies between them (e.g., fragments must
exist before components can query them) determine execution order. Never edit
generated .d.ts files directly — run npm run codegen to regenerate after
GraphQL changes.
Key Patterns
requiresComponentsas detection flag — single boolean onProductVariantthat identifies bundles across storefront pages and cart.- Aliased query field (
isBundle) — keeps bundle logic isolated from primary variant selection. componentsconnection — providesProductVariantComponentnodes with variant data and quantity for rendering bundled items.- Badge overlay —
BundleBadgeuses absolute positioning; parent containers must setposition: relative. - Conditional CTA text —
ProductFormadjusts button copy based onisBundleprop.
Customized Bundles: Additional Requirements
Customized bundles require a Cart Transform Function (cart_transform
Shopify Function, API 2025-07+) with two operations:
- Expand — transforms a bundle line item into its component lines at checkout.
- Merge (
linesMerge) — when all components of a bundle are present in the cart, combines them into the parent variant.
Activated via cartTransformCreate mutation. Requires write_cart_transforms +
write_products access scopes. Implemented in Rust or JS/TS via Shopify CLI
(shopify app generate extension --template cart_transform).
Component Data Sources
| Approach | Security | Use case | Notes |
|---|---|---|---|
| Metafields | Secure (server) | Merchant-defined | Recommended; requires metafield definitions |
| Line properties | Browser-editable | Mix-and-match | Requires validation in function |
Metafields are defined under Settings > Custom data > Variants. Key metafields:
| Metafield | Type | Purpose |
|---|---|---|
component_reference | Product Variant — List | Component variant IDs in the bundle |
component_quantities | Integer — List (min 1) | Quantity per component (order matches refs) |
component_parents | JSON | Reverse-lookup: parent bundles a child belongs to |
Shared Behavior (All Types)
- Discounts: calculated on the parent, allocated to components
- Taxes: computed on components, not the parent
- Cart display: grouped in cart/checkout; queryable via
cart.line.components - Fulfillment: no constraint — components can be fulfilled independently
- Refunds/returns: component-level operations
- Reporting: component-level sales reporting
Prerequisites
- The store must meet Shopify’s
eligibility requirements
(
BundlesFeatureGraphQL object can verify programmatically). - Bundles can be created via the first-party Shopify Bundles app or via the Bundles API directly from a custom Shopify app with its own admin UI. The API approach gives full control over the merchant experience and bundle configuration logic.
- At least one bundle must be created before storefront changes take effect.
- For custom storefronts (Hydrogen), bundle publishing must be enabled — disabled by default. Only supported for product fixed bundles. The app must be a sales channel; third-party apps activate via Partner Dashboard (App setup → “Sell Bundles”).
Limitations
- API limits — 3 options per bundle product, 2,048 variants per bundle product, 30 component variants per bundle variant.
- Selling plans — bundles cannot be combined with subscriptions, pre-orders, or try-before-you-buy.
- Shopify Scripts — line item Scripts don’t apply to bundle line items. Scripts sunset June 30, 2026 — migrate to Shopify Functions.
- No nesting — a bundle can’t be a component of another bundle.
- App-exclusive management — once an app assigns components, only that app can modify them.
- Custom storefront publishing — only product fixed bundles; variant fixed bundles cannot be published.
Troubleshooting
| Issue | Solution |
|---|---|
| No bundles visible on storefront | Create bundles in admin (via Shopify Bundles app or custom app using the Bundles API) |
| No badges on product pages | Verify PRODUCT_FRAGMENT includes the isBundle alias and BundledVariants is rendered |
| No badges in cart | Verify CART_QUERY_FRAGMENT includes requiresComponents and components |
See Also
- combined-listings — Shopify combined listings: parent-child structure, limits, Admin API
- hydrogen-development — Hydrogen learning curriculum
- Hydrogen Bundles Recipe (source)
- Hydrogen Bundles LLM Prompt (source)
- Shopify Bundles Overview (Source)
- Start Building Bundles (Source)
- Variant-Fixed Bundles with Combined Listings (note)
- Shopify Bundles API docs