Bundle Builder
Build-your-own bundles with tiered discount. Replaces Bold Bundles.
Live preview
See it in action.
Fully interactive, drag, click, scroll inside the frame, toggle to mobile.
About this section
A complete pick-N-from-collection bundle builder with live progress bar, real-time discount calculation, and one-click add-all-to-cart. Pure Liquid + Cart AJAX. Pair with a Shopify automatic discount keyed on the `_bundle` line property to apply the percentage at checkout.
Install in 90 seconds
- 01
Create /sections/modblo-bundle-builder.liquid.
- 02
Paste the section code and save.
- 03
Add the section to any template via the theme editor.
- 04
In Shopify Admin → Discounts, create an automatic order discount that targets line items with the property `_bundle` set, matching the percentage you configured here.
- 05
(Optional) Limit by collection so the discount only applies when bundle products are present.
The Liquid
{%- comment -%}
modblo. Bundle Builder
Replaces Bold Bundles / Bundler / Easy Bundles apps.
Pick N items from a collection, get a tiered discount.
Pure Liquid + Cart AJAX, no third-party SDKs.
{%- endcomment -%}
{%- assign source = collections[section.settings.source_collection] -%}
{%- assign target_qty = section.settings.target_qty | default: 3 -%}
{%- assign discount_pct = section.settings.discount_pct | default: 15 -%}
<section class="modblo-bb" data-modblo-bb
data-target="{{ target_qty }}"
data-discount="{{ discount_pct }}"
data-section-id="{{ section.id }}"
style="--modblo-bb-bg: {{ section.settings.bg }};
--modblo-bb-fg: {{ section.settings.fg }};
--modblo-bb-accent: {{ section.settings.accent }};
--modblo-bb-bar: {{ section.settings.bar_color }};">
<div class="modblo-bb__inner page-width">
<header class="modblo-bb__head">
<p class="modblo-bb__eyebrow">{{ section.settings.eyebrow }}</p>
<h2 class="modblo-bb__h">{{ section.settings.heading }}</h2>
<p class="modblo-bb__sub">{{ section.settings.subheading }}</p>
</header>
<div class="modblo-bb__layout">
{%- comment -%} Product picker grid {%- endcomment -%}
<div class="modblo-bb__grid" data-modblo-bb-grid>
{%- for product in source.products limit: 12 -%}
{%- assign first_var = product.selected_or_first_available_variant -%}
<article class="modblo-bb__card" data-modblo-bb-card
data-variant-id="{{ first_var.id }}"
data-price="{{ first_var.price }}"
data-title="{{ product.title | escape }}"
data-image="{{ product.featured_image | image_url: width: 200 }}">
{%- if product.featured_image -%}
{{ product.featured_image | image_url: width: 320 | image_tag:
loading: 'lazy', widths: '160,320', sizes: '160px',
class: 'modblo-bb__card-img' }}
{%- endif -%}
<div class="modblo-bb__card-body">
<p class="modblo-bb__card-title">{{ product.title }}</p>
<p class="modblo-bb__card-price">{{ first_var.price | money }}</p>
</div>
<button type="button" class="modblo-bb__card-add" data-modblo-bb-add aria-label="Add to bundle">
<span class="modblo-bb__card-add-plus">+</span>
<span class="modblo-bb__card-add-check" aria-hidden="true">✓</span>
</button>
</article>
{%- endfor -%}
</div>
{%- comment -%} Bundle summary panel, sticky on desktop {%- endcomment -%}
<aside class="modblo-bb__panel">
<div class="modblo-bb__progress">
<div class="modblo-bb__progress-row">
<span data-modblo-bb-progress-text>Pick {{ target_qty }} items</span>
<strong>
<span data-modblo-bb-count>0</span> / {{ target_qty }}
</strong>
</div>
<div class="modblo-bb__bar">
<div class="modblo-bb__bar-fill" data-modblo-bb-bar></div>
</div>
</div>
<ul class="modblo-bb__selected" data-modblo-bb-selected aria-live="polite"></ul>
<div class="modblo-bb__totals">
<div class="modblo-bb__total-row">
<span>Subtotal</span>
<span data-modblo-bb-subtotal>{{ 0 | money }}</span>
</div>
<div class="modblo-bb__total-row modblo-bb__total-row--discount" data-modblo-bb-discount-row hidden>
<span>Bundle discount ({{ discount_pct }}%)</span>
<span data-modblo-bb-discount>−{{ 0 | money }}</span>
</div>
<div class="modblo-bb__total-row modblo-bb__total-row--final">
<span>You pay</span>
<strong data-modblo-bb-final>{{ 0 | money }}</strong>
</div>
</div>
<button type="button" class="modblo-bb__cta" data-modblo-bb-add-bundle disabled>
Add bundle to cart
</button>
<p class="modblo-bb__fine">Discount applied automatically at checkout</p>
</aside>
</div>
</div>
</section>
<style>
.modblo-bb { background: var(--modblo-bb-bg, #fff); color: var(--modblo-bb-fg, #0b0b0c); padding: clamp(48px, 7vw, 96px) 0; }
.modblo-bb__inner { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
.modblo-bb__head { text-align: center; margin-bottom: 40px; }
.modblo-bb__eyebrow { text-transform: uppercase; letter-spacing: .18em; font-size: 12px; font-weight: 700; color: var(--modblo-bb-accent, #6366f1); margin: 0 0 12px; }
.modblo-bb__h { font-size: clamp(28px, 4vw, 44px); letter-spacing: -.02em; margin: 0 0 12px; line-height: 1.1; }
.modblo-bb__sub { font-size: 16px; opacity: .65; max-width: 560px; margin: 0 auto; }
.modblo-bb__layout { display: grid; grid-template-columns: 1fr 360px; gap: 32px; align-items: start; }
.modblo-bb__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; }
.modblo-bb__card {
position: relative; background: color-mix(in oklab, var(--modblo-bb-fg) 4%, transparent);
border-radius: 16px; padding: 12px; cursor: pointer;
border: 2px solid transparent; transition: border-color .2s, transform .2s;
}
.modblo-bb__card:hover { transform: translateY(-2px); }
.modblo-bb__card.is-selected { border-color: var(--modblo-bb-accent, #6366f1); }
.modblo-bb__card-img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 10px; margin-bottom: 10px; display: block; }
.modblo-bb__card-body { padding: 0 4px 8px; }
.modblo-bb__card-title { font-size: 13px; font-weight: 600; margin: 0 0 4px; line-height: 1.3; }
.modblo-bb__card-price { font-size: 12px; opacity: .7; margin: 0; }
.modblo-bb__card-add {
position: absolute; top: 18px; right: 18px;
width: 32px; height: 32px; border-radius: 50%;
background: var(--modblo-bb-bg, #fff); color: var(--modblo-bb-fg, #0b0b0c);
border: 1px solid color-mix(in oklab, var(--modblo-bb-fg) 15%, transparent);
cursor: pointer; display: grid; place-items: center;
font-size: 18px; line-height: 1; transition: background .2s, color .2s;
box-shadow: 0 2px 8px rgba(0,0,0,.06);
}
.modblo-bb__card-add-check { display: none; font-size: 14px; }
.modblo-bb__card.is-selected .modblo-bb__card-add { background: var(--modblo-bb-accent, #6366f1); color: #fff; border-color: transparent; }
.modblo-bb__card.is-selected .modblo-bb__card-add-plus { display: none; }
.modblo-bb__card.is-selected .modblo-bb__card-add-check { display: inline; }
.modblo-bb__panel {
background: color-mix(in oklab, var(--modblo-bb-fg) 4%, transparent);
border-radius: 20px; padding: 24px; position: sticky; top: 24px;
}
.modblo-bb__progress-row { display: flex; justify-content: space-between; font-size: 14px; margin-bottom: 10px; }
.modblo-bb__bar { height: 8px; border-radius: 999px; background: color-mix(in oklab, var(--modblo-bb-fg) 10%, transparent); overflow: hidden; }
.modblo-bb__bar-fill { height: 100%; width: 0%; background: var(--modblo-bb-bar, #16a34a); border-radius: 999px; transition: width .3s cubic-bezier(.16,1,.3,1); }
.modblo-bb__selected { list-style: none; padding: 16px 0 0; margin: 16px 0 0; border-top: 1px solid color-mix(in oklab, var(--modblo-bb-fg) 8%, transparent); display: flex; flex-direction: column; gap: 10px; min-height: 60px; }
.modblo-bb__selected:empty::before { content: 'Your bundle is empty'; opacity: .45; font-size: 13px; }
.modblo-bb__selected li { display: flex; align-items: center; gap: 10px; font-size: 13px; }
.modblo-bb__selected img { width: 36px; height: 36px; border-radius: 6px; object-fit: cover; }
.modblo-bb__selected .modblo-bb__sel-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
.modblo-bb__selected .modblo-bb__sel-remove { background: transparent; border: 0; cursor: pointer; opacity: .5; padding: 4px; font-size: 16px; line-height: 1; }
.modblo-bb__selected .modblo-bb__sel-remove:hover { opacity: 1; }
.modblo-bb__totals { padding: 16px 0; margin: 16px 0 0; border-top: 1px solid color-mix(in oklab, var(--modblo-bb-fg) 8%, transparent); }
.modblo-bb__total-row { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px; }
.modblo-bb__total-row--discount { color: color-mix(in oklab, var(--modblo-bb-bar, #16a34a) 100%, black 0%); }
.modblo-bb__total-row--final { font-size: 15px; padding-top: 8px; border-top: 1px solid color-mix(in oklab, var(--modblo-bb-fg) 8%, transparent); margin-top: 10px; margin-bottom: 0; }
.modblo-bb__total-row--final strong { font-size: 18px; }
.modblo-bb__cta {
width: 100%; margin-top: 16px; padding: 14px 22px;
background: var(--modblo-bb-accent, #6366f1); color: #fff;
border: 0; border-radius: 12px; font-size: 15px; font-weight: 600;
cursor: pointer; transition: opacity .2s, transform .2s;
}
.modblo-bb__cta:hover:not([disabled]) { opacity: .92; }
.modblo-bb__cta:active:not([disabled]) { transform: scale(.98); }
.modblo-bb__cta[disabled] { opacity: .45; cursor: not-allowed; }
.modblo-bb__fine { font-size: 11px; opacity: .55; margin: 8px 0 0; text-align: center; }
@media (max-width: 880px) {
.modblo-bb__layout { grid-template-columns: 1fr; }
.modblo-bb__panel { position: static; }
.modblo-bb__grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
}
</style>
<script>
(function () {
var root = document.querySelector('[data-modblo-bb][data-section-id="{{ section.id }}"]');
if (!root) return;
var target = parseInt(root.dataset.target, 10) || 3;
var discountPct = parseInt(root.dataset.discount, 10) || 15;
var picked = [];
var grid = root.querySelector('[data-modblo-bb-grid]');
var countEl = root.querySelector('[data-modblo-bb-count]');
var barEl = root.querySelector('[data-modblo-bb-bar]');
var selEl = root.querySelector('[data-modblo-bb-selected]');
var subEl = root.querySelector('[data-modblo-bb-subtotal]');
var discRow = root.querySelector('[data-modblo-bb-discount-row]');
var discEl = root.querySelector('[data-modblo-bb-discount]');
var finalEl = root.querySelector('[data-modblo-bb-final]');
var ctaEl = root.querySelector('[data-modblo-bb-add-bundle]');
var progressTextEl = root.querySelector('[data-modblo-bb-progress-text]');
function fmt(cents) { return '$' + (cents / 100).toFixed(2); }
function render() {
var qty = picked.length;
countEl.textContent = qty;
barEl.style.width = Math.min(100, (qty / target) * 100) + '%';
if (qty < target) {
progressTextEl.textContent = 'Pick ' + (target - qty) + ' more';
} else {
progressTextEl.textContent = '🎉 Bundle ready';
}
selEl.innerHTML = picked.map(function (p, i) {
return '<li>' +
(p.image ? '<img src="' + p.image + '" alt="">' : '') +
'<span class="modblo-bb__sel-title">' + p.title + '</span>' +
'<button type="button" class="modblo-bb__sel-remove" data-modblo-bb-remove="' + i + '" aria-label="Remove">×</button>' +
'</li>';
}).join('');
var subtotal = picked.reduce(function (s, p) { return s + p.price; }, 0);
var discount = qty >= target ? Math.round(subtotal * discountPct / 100) : 0;
var final = subtotal - discount;
subEl.textContent = fmt(subtotal);
discEl.textContent = '−' + fmt(discount);
finalEl.textContent = fmt(final);
discRow.hidden = discount === 0;
ctaEl.disabled = qty < target;
// Wire remove buttons
selEl.querySelectorAll('[data-modblo-bb-remove]').forEach(function (btn) {
btn.addEventListener('click', function () {
var idx = parseInt(btn.dataset.cbBbRemove, 10);
var removed = picked.splice(idx, 1)[0];
var card = grid.querySelector('[data-variant-id="' + removed.id + '"]');
if (card) card.classList.remove('is-selected');
render();
});
});
}
grid.querySelectorAll('[data-modblo-bb-card]').forEach(function (card) {
card.addEventListener('click', function (e) {
e.preventDefault();
var id = card.dataset.variantId;
var idx = picked.findIndex(function (p) { return p.id === id; });
if (idx >= 0) {
picked.splice(idx, 1);
card.classList.remove('is-selected');
} else {
if (picked.length >= target) return;
picked.push({
id: id,
price: parseInt(card.dataset.price, 10),
title: card.dataset.title,
image: card.dataset.image,
});
card.classList.add('is-selected');
}
render();
});
});
ctaEl.addEventListener('click', function () {
if (picked.length < target) return;
ctaEl.disabled = true;
ctaEl.textContent = 'Adding…';
var items = picked.map(function (p) {
return { id: p.id, quantity: 1, properties: { _bundle: '{{ section.id }}' } };
});
fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: items }),
}).then(function () {
window.location.href = '/cart';
}).catch(function () {
ctaEl.disabled = false;
ctaEl.textContent = 'Add bundle to cart';
});
});
render();
})();
</script>
{% schema %}
{
"name": "Bundle Builder",
"tag": "section",
"settings": [
{ "type": "header", "content": "Source" },
{ "type": "collection", "id": "source_collection", "label": "Source collection",
"info": "Customers pick from products in this collection." },
{ "type": "header", "content": "Bundle rules" },
{ "type": "range", "id": "target_qty", "label": "Items per bundle", "min": 2, "max": 6, "step": 1, "default": 3 },
{ "type": "range", "id": "discount_pct", "label": "Discount %", "min": 5, "max": 50, "step": 5, "default": 15,
"info": "Apply matching automatic discount in Shopify Admin → Discounts using the line property `_bundle`." },
{ "type": "header", "content": "Copy" },
{ "type": "text", "id": "eyebrow", "label": "Eyebrow", "default": "Build your bundle" },
{ "type": "text", "id": "heading", "label": "Heading", "default": "Pick 3, save 15%" },
{ "type": "text", "id": "subheading", "label": "Subheading", "default": "Mix and match, discount applied at checkout." },
{ "type": "header", "content": "Colors" },
{ "type": "color", "id": "bg", "label": "Background", "default": "#ffffff" },
{ "type": "color", "id": "fg", "label": "Foreground", "default": "#0b0b0c" },
{ "type": "color", "id": "accent", "label": "Accent", "default": "#6366f1" },
{ "type": "color", "id": "bar_color", "label": "Progress bar", "default": "#16a34a" }
],
"presets": [{ "name": "Bundle Builder" }]
}
{% endschema %}Unlock the section code
Bundle Builder is a premium section. Get the full Liquid + scoped CSS paste-ready.
One-time purchase · Lifetime updates · You own the code
Theme editor settings
| Setting | Type | Default |
|---|---|---|
Source collection source_collection | collection | , |
Items per bundle target_qty | range | 3 |
Discount % discount_pct | range | 15 |
Eyebrow eyebrow | text | Build your bundle |
Heading heading | text | Pick 3, save 15% |
Subheading subheading | text | Mix and match, discount applied at checkout. |
Background bg | color | #ffffff |
Foreground fg | color | #0b0b0c |
Accent accent | color | #6366f1 |
Progress bar color bar_color | color | #16a34a |
SEO & accessibility notes
- Built on Cart AJAX (/cart/add.js), works with all theme stacks.
- Real <button> elements on each card, keyboard navigable.
- ARIA live region announces selection updates to screen readers.
- Progress bar uses width transition only, no layout thrash, smooth on low-end devices.
Related
